//********************************************************************************
//* File       : Salmon.cpp                                                      *
//* Author     : Mahlon R. Smith                                                 *
//*              Copyright (c) 2012-2026 Mahlon R. Smith, The Software Samurai   *
//*                  GNU GPL copyright notice below.                             *
//* Date       : 29-May-2026                                                     *
//* Version    : (see AppVersion in Salmon.hpp)                                  *
//*                                                                              *
//* Description: This is a simple application to test the functionality of the   *
//* 'exec' group of C library functions.                                         *
//*                                                                              *
//* Development Tools:                                                           *
//* Fedora 43     GNOME Terminal v:3.56.3    GCC (G++) v:15.2.1 20260123         *
//* Fedora 30     GNOME Terminal v:3.32.2    GCC (G++) v: 9.2.1 20190827         *
//* Fedora 20     GNOME Terminal v:3.2.1     GCC (G++) v: 4.8.3 20140911         *
//* Fedora 12     GNOME Terminal v:2.x       GCC (G++) v: 4.4.4 20100429         *
//********************************************************************************
//* Copyright Notice:                                                            *
//* This program is free software: you can redistribute it and/or modify it      *
//* under the terms of the GNU 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 General Public License     *
//* for more details.                                                            *
//*                                                                              *
//* You should have received a copy of the GNU General Public License along      *
//* with this program.  If not, see <http://www.gnu.org/licenses/>.              *
//*                                                                              *
//********************************************************************************
//* Version History (most recent first):                                         *
//*                                                                              *
//* v: 0.0.05 16-May-2026                                                        *
//*    -- Convert from a pseudo-C application to a class-based application.      *
//*       All functionality is now inplemented within the Salmon class.          *
//*    -- Because X11 is no longer smiled upon by the X-gods (or Xodds), the     *
//*       algorithm has been updated to assume that the various X utilities are  *
//*       no longer available. Because Wayland refuses to provide much of the    *
//*       information needed for this application; we now directly query the     *
//*       system files for the emulator name and shell name.                     *
//*       The capture of this information is adapted from the author's           *
//*       AnsiCmd library package.                                               *
//*    -- In previous releases, the application's temporary files were created   *
//*       in the current working directory (CWD). Since the standard library     *
//*       now supports creation and deletion of temporary files. the application *
//*       has been updated to take advantage of the new library functionality.   *
//*       This required updating the minimum compiler standard from              *
//*             -std=gnu++11 to -std=gnu++17.                                    *
//*    -- We have resisted the integrating the author's gString class into this  *
//*       application for string analysis and formatting. This application is    *
//*       intended to be a very simple example, but string manipulaton has become*
//*       a key aspect of its functionality. The C-library primitives are        *
//*       dangerous at best, and using them for anything more than simple        *
//*       console I/O is not worth the pain. At the same time, the C++ 'string'  *
//*       and 'wstring' classes just clog the flow of a designer's creative      *
//*       juices like a whale carcas rotting on the beach.                       *
//*       For this reason, we have replaced all C library string functions with  *
//*       the author's gString class. It is politely recommended that all        *
//*       developers do the same with their applications.                        *
//*    -- Combine the functionality of the separate makefiles so that both the   *
//*       parent and child applications are built using the same makefile.       *
//*       See "Makefile" for details.                                            *
//*    -- Update documentation.                                                  *
//*    -- Posted to website: ??-???-2026                                         *
//*                                                                              *
//* v: 0.0.04 27-Sep-2019                                                        *
//*    -- The compiler upgrade to GCC (G++) v:9.0.1 introduced some new          *
//*       warning messages. Specifically, the C function snprintf() now causes   *
//*       complaints about potential buffer overrun.                             *
//*            -Wformat-truncation, pragma -Wno-format-truncation                *
//*       We now test for output truncation in snprintf() calls which use        *
//*       an "%s" in the formatting template. Because our data are tightly       *
//*       controlled, this warning is more of an inconvenience than an actual    *
//*       problem, so we do not actually perform error recovery, but in the      *
//*       spirit of best practices we have tweaked the code slightly to          *
//*       silence the warnings. (Note that C formatting functions are too        *
//*       dangerous to be used in a "real" application, but here it's OK.)       *
//*    -- Fedora's move from Xorg to the Wayland compositor has caused a         *
//*       mountain of problems for everyone, and it is no different for this     *
//*       application. Information that was once easily obtainable under the     *
//*       X11 protocol is now literally impossible to obtain. All this is being  *
//*       done (theoretically) in the interest of security and hardware          *
//*       independence, but this independence comes at a price.                  *
//*       Please see the notes in DefineChildEmulator() method.                  *
//*       To overcome Wayland's shortcommings, we are forced to ask you, the     *
//*       user to tell us which terminal emulator to launch in response to the   *
//*       "-t" command-line option.                                              *
//*    -- Konsole no longer accepts the "geometry" parameter. Formerly, it       *
//*       just ignored the parameter, but now it chokes.                         *
//*    -- Documentation update.                                                  *
//*    -- Posted to website 02-Oct-2019.                                         *
//*                                                                              *
//* v: 0.0.03 04-Jun-2017                                                        *
//*    -- Documentation update only.                                             *
//*                                                                              *
//* v: 0.0.02 05-May-2017 With enhancements                                      *
//*    -- Add command-line help.                                                 *
//*    -- Add option '-s' to spawn a child process with stdout and stderr        *
//*       redirected to files. This is an experiment in launching an arbitrary   *
//*       external application from within another application without           *
//*       disturbing the parent's display data.                                  *
//*    -- Add option '-t' to spawn a child process which open a new terminal     *
//*       window based on the terminal emulator and shell program of the         *
//*       parent process.                                                        *
//*    -- Enhance the existing options to more clearly demonstrate the           *
//*       similarities and differences of the various 'exec' functions.          *
//*    -- Create a README file and basic HTML documentation.                     *
//*                                                                              *
//* v: 0.0.01 25-Aug-2012 First pass.                                            *
//*                        Created using GNU G++ (Gcc v: 4.4.2)                  *
//*                        under Fedora Linux, 2.6.31-5-127.fs12.i686.PAE        *
//*    -- We apologize for the application name. This a project written          *
//*       strictly for fun. "Salmon" swim upstream to spawn fish eggs, not       *
//*       process IDs, but we hope you get the idea.                             *
//********************************************************************************
//* Programmer's Notes:                                                          *
//*                                                                              *
//* Please see the documentation for more extensive notes.                       *
//*                                                                              *
//* Exec functions called by this application:                                   *
//* ------------------------------------------                                   *
//* '-v' option:                                                                 *
//*    int execv ( const char* fileName, char const* argV[] ) ;                  *
//*        argV[0] s/b bare filename                                             *
//*        argV[1], ...  additional arguments                                    *
//*        argV[n] NULL*                                                         *
//*                                                                              *
//* '-l' and '-f' options:                                                       *
//* int execl ( const char* fileName, const char* argVa, ... ) ;                 *
//*     argVa   s/b bare filename                                                *
//*     argVb, ...  addional arguments                                           *
//*     argVn   NULL*                                                            *
//*                                                                              *
//* '-p' and '-t' options:                                                       *
//* int execlp ( const char* fileName, const char* argVa, ... ) ;                *
//*     same as execl, except it searches the PATH for fileName                  *
//*                                                                              *
//* '-s' option:                                                                 *
//* int execvp ( const char* fileName, char *const argV[] ) ;                    *
//*     same as execv, except it searches the PATH for fileName                  *
//*                                                                              *
//********************************************************************************

//** Include files **
#include "Salmon.hpp"

//** Local Definitions and Data **
static gString gsdbg ;  // used during development and for generic text formatting

//* Local prototypes *
static void dumpEmuData ( const emuData& emu, bool verbose = false, const char* msg = NULL ) ;
static bool extractFirstLine ( const char* src, gString& trg ) ;
static void extractNumericSubstring ( const gString& src, gString& trg ) ;
static void sboxScreenReport ( const char* tempSpec ) ;


//*************************
//*        main           *
//*************************
//********************************************************************************
//* Program entry point.                                                         *
//*                                                                              *
//* Command-line Usage: see the helpMe() method                                  *
//*                                                                              *
//********************************************************************************

int main ( int argc, char const* argv[], char const* argenv[] )
{

   //* Instantiate the application class *
   Salmon *salmon = new Salmon ( argc, argv ) ;

   delete salmon ;         // release the dynamic allocation

   exit ( ZERO ) ;

}  //* End main() *

//*************************
//*        Salmon         *
//*************************
//********************************************************************************
//* Initialization constructor:                                                  *
//* Decode command-line arguments and execute the specified test.                *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* Input  : argc  : number of command-line arguments                            *
//*          argv  : array of pointers to command-line arguments                 *
//*                                                                              *
//* Returns: implicitly returns pointer to class                                 *
//********************************************************************************

Salmon::Salmon ( int argc, char const* argv[] )
{
   SalTest stest = stHelp ;      // default option
   bool validTest = false ;      // set if valid command-line syntax
   this->reset () ;              // initialize data members

   system ( "clear" ) ;          // clear the decks

   //* Decode the command line argument(s) *
   if ( argc > 1 && argv[1][0] == DASH && argv[1][1] != NULLCHAR )
   {
      validTest = true ;                  // hope for the best

      switch ( tolower(argv[1][1]) )
      {
         case 'v':   stest = stExecV ;       break ;
         case 'l':   stest = stExecL ;       break ;
         case 'p':   stest = stExecLP ;      break ;
         case 'f':   stest = stExecForkL ;   break ;
         case 's':   stest = stExecForkVP ;  break ;
         case 't':   stest = stExecForkLP ;  break ;
         case 'd':   stest = stSandbox ;     break ;
         case '-':
            if ( argv[1][2] == 'v' )         // version
               stest = stVersion ;
            else if ( argv[1][2] == 'h' )    // help (redundant)
               stest = stHelp ;
            validTest = false ;
            break ;
         default:
            validTest = false ;
            break ;
      }
   }

   if ( validTest )
   {
      //* Say hello and report the user's command-line arguments *
      wcout << "Salmon Swim Upstream To Spawn!\n"
            << " Invocation: \"" ;
      for ( short i = ZERO ; i < argc ; ++i )
      {
         if ( i > 1 )  wcout << L"'" ;
         wcout << argv[i] ;
         if ( i > 1 ) wcout << L"'" ;
         if ( i < (argc - 1) )
            wcout << " " ;
      }
      this->pPid = getpid() ;             // get process ID
      wcout << "\"\n" 
            << " Parent PID: " << this->pPid << endl ;

      this->argCnt  = argc ;              // make arguments available to all tests
      this->argList = argv ;
      this->getBasePath ( argv[0] ) ;     // save base application directory
                                          // and construct filespec of child application

      switch ( stest )                    // execute the specified test
      {
         case stExecV:      this->Test_execv () ;        break ;
         case stExecL:      this->Test_execl () ;        break ;
         case stExecLP:     this->Test_execlp () ;       break ;
         case stExecForkL:  this->Test_Fork_execl () ;   break ;
         case stExecForkVP: this->Test_Fork_execvp () ;  break ;
         case stExecForkLP: this->Test_Fork_execlp () ;  break ;
         case stSandbox:    this->Sandbox () ;           break ;
         default:                         // (this would be a program error)
            break ;
      } ;
   }
   else if ( stest == stVersion )
      this->reportVersion () ;
   else
      this->helpMe () ;

}  //* End Salmon() *

//*************************
//*      Test_execv       *
//*************************
//********************************************************************************
//* Test 'v':                                                                    *
//* ---------                                                                    *
//* Invoke a child process using the 'execv' function.                           *
//* Arguments are passed to the external application as an array of pointers.    *
//* After the external application is invoked, the parent (this application)     *
//* terminates.                                                                  *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void Salmon::Test_execv ( void )
{
   //* Default arguments for calling the child application *
   const char* trgSwitch = "-v" ;
   const char* trgTitle  = "Types of Salmon:" ;
   const char* salmonA   = "Trout" ;
   const char* salmonB   = "Char" ;
   const char* salmonC   = "Grayling" ;
   const char* salmonD   = "Whitefish" ;
   const char* avArray[] = 
   {
      childApp,      // first argument (argv[0]) is the name of the application
      trgSwitch,
      trgTitle,
      salmonA,
      salmonB,
      salmonC,
      salmonD,
      NULL
   } ;

   //* If user has sent us at least one non-option argument,  *
   //* replace static arguments with user's arguments (max 4).*
   if ( this->argCnt > 2 )
      avArray[3] = this->argList[2] ;
   if ( this->argCnt > 3 )
      avArray[4] = this->argList[3] ;
   if ( this->argCnt > 4 )
      avArray[5] = this->argList[4] ;
   if ( this->argCnt > 5 )
      avArray[6] = this->argList[5] ;

   wcout << " calling execv( childApp, argv[] )\n" << endl ;

   //* Invoke the external application (note the odd syntax) *
   execv ( this->childFSpec, (char* const*)avArray ) ;

   //* If execution continues here, it means that the invocation failed. *
   wcout << " Error! execv() failed." << endl ;

}  //* End Test_execv() *

//*************************
//*      Test_execl       *
//*************************
//********************************************************************************
//* Test 'l':                                                                    *
//* ---------                                                                    *
//* Invoke a child process using the 'execl' function.                           *
//* Arguments are passed to the external application individually.               *
//* By default, only the constant values for argv[0] and argv[1] are passed      *
//* to the child process; however, if auxilliary arguments are provided on the   *
//* command line (max 4), they are also passed on to the child process.          *
//*                                                                              *
//* After the external application is invoked, the parent (this application)     *
//* terminates.                                                                  *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void Salmon::Test_execl ( void )
{
   //* If execl() call is successful, this application terminates.    *
   wcout << " calling execl( childApp ... )\n" << endl ;

   //* If user has sent us non-option arguments (max 4), pass them on.*
   int status = execl ( this->childFSpec, childApp, "-l", 
                        (this->argCnt > 2 ? this->argList[2] : NULL),
                        (this->argCnt > 3 ? this->argList[3] : NULL),
                        (this->argCnt > 4 ? this->argList[4] : NULL),
                        (this->argCnt > 5 ? this->argList[5] : NULL),
                        NULL ) ;

   //* If execution continues here, it means that the invocation failed. *
   if ( status == -1 )
      wcout << " Error! execl() failed." << endl ;

}  //* End Test_execl() *

//*************************
//*      Test_execlp      *
//*************************
//********************************************************************************
//* Test 'p':                                                                    *
//* ---------                                                                    *
//* Invoke a child process using the 'execlp' function.                          *
//* Arguments are passed to the external application individually.               *
//* The 'grep' utility is invoked. 'grep' should always be visible on the system *
//* 'path' (echo $PATH)                                                          *
//*                                                                              *
//* By default, three arguments are passed to grep:                              *
//*   1) "-n" indicates that line numbers for matching item should be displayed. *
//*   2) "COMPILE" is the string for which to search.                            *
//*   3) "Makefile" is the source file to be searched.                           *
//* If alternate search string and source file(s) are specified, they replace    *
//* default values 2 and 3. Example:                                             *
//*   ./salmon -p 'AppVersion' Salmon.cpp Salmon.hpp                             *
//*                                                                              *
//* Technical Note: The shell may play games will data enclosed in single or     *
//* double quotes or with special characters. For instance,the following         *
//* invocation works as expected. The shell correctly interprets the search      *
//* string and expansion of the regular expression, (although for technical      *
//* reasons, this method is unable to report expansion of the regular expresion).*
//*   ./salmon -p "AppVersion =" Salmon.[hc]pp                                   *
//*                                                                              *
//* After the external application is invoked, the parent (this application)     *
//* terminates.                                                                  *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void Salmon::Test_execlp ( void )
{
   const char* searchString = "COMPILE" ;
   const char* sourceFile1  = "Makefile" ;
   const char* arg3         = NULL ;
   const char* arg4         = NULL ;

   //* If user has sent us non-option arguments (max 4), pass them on *
   //* to the child application. Else, send default arguments.        *
   if ( this->argCnt > 2 )
      searchString = this->argList[2] ;
   if ( this->argCnt > 3 )
      sourceFile1 = this->argList[3] ;
   if ( this->argCnt > 4 )
      arg3 = this->argList[4] ;
   if ( this->argCnt > 5 )
      arg4 = this->argList[5] ;

   wcout << L" calling execlp( grep -n '" 
         << searchString << L"' " 
         << sourceFile1  << L" )\n" << endl ;

   //* Invoke the system utility. *
   execlp ( "grep", "grep", "-n", searchString, sourceFile1, 
            arg3, arg4, NULL ) ;

   //* If execution continues here, it means that the invocation failed. *
    wcout << " Error! execlp() failed." << endl ;

}  //* End Test_execlp() *

//*************************
//*    Test_Fork_execl    *
//*************************
//********************************************************************************
//* Test 'f':                                                                    *
//* ---------                                                                    *
//* Invoke a child process using the 'execl' function.                           *
//* Arguments are passed to the external application individually.               *
//* By default, only the constant values for argv[0] and argv[1] are passed to   *
//* the child process; however, if auxilliary arguments are provided on the      *
//* command line (max 4), they are also passed on to the child process.          *
//*                                                                              *
//* The parent (this application) generates a new process with a unique          *
//* process ID. This new process invokes the external application, and the       *
//* parent process goes to sleep until the child process terminates.             *
//* After the child process terminates, the parent continues operation.          *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void Salmon::Test_Fork_execl ( void )
{
   gString gs ;
   wcout << " Spawn a child process using fork(), then call execl( childApp ... )\n" 
         << endl ;

   pid_t fpid = vfork () ;       // create the child process

   if ( fpid == ZERO)
   {  //* The child process (if created) executes here.                  *
      //* If user has sent us non-option arguments (max 4), pass them on *
      //* to the child application. Else, send only argv[0] and argv[1]. *
      int status = execl ( this->childFSpec, childApp, "-f", 
                           (this->argCnt > 2 ? this->argList[2] : NULL),
                           (this->argCnt > 3 ? this->argList[3] : NULL),
                           (this->argCnt > 4 ? this->argList[4] : NULL),
                           (this->argCnt > 5 ? this->argList[5] : NULL),
                           NULL ) ;
      if ( status == -1 )
         wcout << " Error! execl() failed." << endl ;

      //* In case the exec call fails: child process MUST NOT continue.*
      _exit ( 0 ) ;
   }
   else
   {  //* The parent process continues execution here *
      if ( fpid > ZERO )      // child successfully launched
      {
         int childStatus = ZERO ;   // receives child's exit status
         int statusOptions = ZERO ; // status bitmask (none defined at this time)

         //* Sleep until the child process terminates.  *
         //* waitpid() _should_ return the child's PID, *
         //* but if not, then check errno for:          *
         //* EINTR || ECHILD || EINVAL                  *
         int  rval = waitpid ( fpid, &childStatus, statusOptions ) ;

         gs.compose( "Parent (%d) has regained control.", &this->pPid ) ;
         wcout << gs << endl ;
         if ( rval == fpid )
         {
            gs.compose( "Child process %d has terminated with a status of 0x%04X", 
                        &rval, &childStatus ) ;
            wcout << gs << endl ;
         }
         else
         {
            gs.compose( "waitpid() has returned an error condition. errno: 0x%04X", &errno ) ;
            wcout << gs << endl ;
         }
      }
      else
      {
         wcout << " fork() failed. PID: " << fpid << endl ;
      }
   }

}  //* End Test_Fork_execl() *

//*************************
//*   Test_Fork_execvp    *
//*************************
//********************************************************************************
//* Test 's':                                                                    *
//* ---------                                                                    *
//* Invoke a child process using the 'execvp' function.                          *
//*                                                                              *
//* The parent (this application) generates a new process with a unique          *
//* process ID. This new process invokes the external application, with all      *
//* output redirected to temporary files: one file receives data from 'stdout',  *
//* and another file receives data from 'stderr'. This means that the child      *
//* process does not write to the terminal window, and therefore does not        *
//* interfere with the operation of the parent process.                          *
//* During invocation, the child process is instructed to write at least one     *
//* message to 'stdout' and at least one message to 'stderr'.                    *
//*                                                                              *
//* The parent process continues operation immediately.                          *
//* Then, after the child process terminates, the parent displays the contents   *
//* of the temp files. This validates the redirection of the streams.            *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void Salmon::Test_Fork_execvp ( void )
{
   const char* childArgs[] =              // child's argv[] array
               {
                  childApp,               // argv[0]
                  "-s",                   // argv[1]
                  "Hello stdout!\n",      // argv[2] (written to stdout)
                  "Hello stderr!\n",      // argv[3] (written to stderr)
                  NULL                    // terminate the list
               } ;
   gString soFName ;                      // stdout target file
   gString seFName ;                      // stderr target file
   int soDesc = ZERO,                     // stdout target descriptor
       seDesc  ;                          // stderr target descriptor

   //* If the system's temp-file directory has not yet been identified *
   //* do it now. Then create two temporary files.                     *
   if ( *this->tfPath == NULLCHAR )
      this->getTempfilePath () ;
   this->createTempfile ( soFName ) ;
   this->createTempfile ( seFName ) ;

   //* If both temporary files created successfully *
   if ( (soFName.gschars() > 1) && (seFName.gschars() > 1) )
   {
      pid_t fpid = vfork () ;       // create the child process

      if ( fpid == ZERO)
      {  //* The child process (if created) executes here.         *
         //* 1) Create temporary files for 'stdout' and 'stderr'.  *
         //* 2) Redirect 'stdout' and 'stderr' to their respective *
         //*    temporary files using the 'dup2()' function.       *
         //* 3) Close the temporary files.                         *
         // Programmer's Note: We open and close these files using 
         // kernel-level file descriptors because getting the underlying 
         // file descriptor from the C++ fstream object is implementation 
         // dependent and therefore unreliable.
         soDesc = open ( soFName.ustr(), O_WRONLY | O_CREAT | O_TRUNC, 
                           S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH ) ;
         seDesc = open ( seFName.ustr(), O_WRONLY | O_CREAT | O_TRUNC, 
                           S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH ) ;
         dup2 ( soDesc, STDOUT_FILENO) ;
         dup2 ( seDesc, STDERR_FILENO) ;
         close ( soDesc ) ;
         close ( seDesc ) ;

         //* If execvp() call is successful, the child process will not *
         //* return to the parent application, else the return value    *
         //* will be written to the terminal.                           *
         //* Possible causes of execvp() failure would be insufficient  *
         //* system resources or maximum number of processes reached.   *
         wcerr << execvp ( this->childFSpec, (char* const* )childArgs ) ;

         wcerr << " Error! Child process call to execvp() failed." << endl ;

         //* In case the exec call fails: child process MUST NOT continue.*
         _exit ( 0 ) ;
      }
      else
      {  //* The parent process continues execution here *
         //* Wait a moment (in case the child PID *
         //* writes an error message to display)  *
         chrono::duration<short, std::milli>aMoment( 250 ) ;
         this_thread::sleep_for( aMoment ) ;
   
         if ( fpid > ZERO )      // child successfully launched
         {
            gString syscmd ;
            wcout << L" Mother salmon spawned child PID: " << fpid << "\n"
                  << L" Her brilliant child wrote to stdout:\n" ;
            wcout.flush() ;
            syscmd.compose( "cat %s", soFName.ustr() ) ;
            system ( syscmd.ustr() ) ;
            wcout << L" And this talented child wrote to stderr:\n" ;
            wcout.flush() ;
            syscmd.compose( "cat %s", seFName.ustr() ) ;
            system ( syscmd.ustr() ) ;
         }
         else
            wcout << " Spawn failed: (" << fpid << "), Mother salmon is childless." << endl ;
      }

      //* If temporary files were created, delete them now. *
      if ( soFName.gschars() > 1 )
         this->deleteTempfile ( soFName ) ;
      if ( seFName.gschars() > 1 )
         this->deleteTempfile ( seFName ) ;
   }
   else
      wcout << L" System Error: Unable to create temporary file(s).\n" << endl ;

}  //* End Test_Fork_execvp() *

//*************************
//*   Test_Fork_execlp    *
//*************************
//********************************************************************************
//* Test 't':                                                                    *
//* ---------                                                                    *
//* Invoke a child process using the 'execlp' function.                          *
//* Launch a child process which will open a new terminal window and will        *
//* execute commands within the new window. The parent process continues         *
//* execution within the current terminal window.                                *
//*                                                                              *
//* NOTES:                                                                       *
//* ------                                                                       *
//*                                                                              *
//* 1) Three terminal emulators are directly supported:                          *
//*     a) gnome-terminal                                                        *
//*     b) konsole                                                               *
//*     c) xterm                                                                 *
//*    The basic terminal-emulator concept is the same for all these             *
//*    applications; however, each of these emulators has somewhat different     *
//*    configuration options and invocation syntax.                              *
//*    Please see the documentation for a discussion of these differences.       *
//*                                                                              *
//* 2) The name of the (current) emulator in which the Salmon application is     *
//*    running and the name of the current shell program are determined by the   *
//*    getEmulator() method. This is referred to as the "parent" window.         *
//*                                                                              *
//* 2) The configuration of the child window is constructed by the               *
//*    DefineChildEmulator() method. This includes the "geometry" for the        *
//*    window, consisting of the width, height and position within the screen.   *
//*    This configuration can be modified using one or more of the optional      *
//*    command-line parameters. See documentation for a discussion of the ways   *
//*    in which these optional arguments are applied to each supported emulator. *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void Salmon::Test_Fork_execlp ( void )
{
   const char* EName[]     // (keep synchronized with enum EmuIndex)
   {
      "emuiGnome",
      "emuiKonsole",
      "emuiXTerm",
      "emuiUnknown",
   } ;

   gString gsGeo, gsCmd, gsCwd, gsOut ;
   char cwd[PATH_MAX], kwid[32], khgt[32] ;
   emuData emu ;                          // emulator/shell information

   if ( (this->argCnt > 2) && (*this->argList[2] == 'h') )
   {
      wcout << L"Test 't' (new Termial), Optional Arguments:\n"
                "ARG1: width of new window in columns, OR\n"
                "      f == open in full-screen mode\n"
                "      t == open new tab in current window\n"
                "      h == hint, i.e. this information\n"
                "ARG2: height of new window in rows\n"
                "ARG3: horizontal offset of window in pixels\n"
                "ARG4: vertical offset of window in pixels\n"
            << endl ;
      return ;    // Note the early return.
   }

   //* Scan the system configuration to define the new terminal window.*
   EmuIndex cmdOption = DefineChildEmulator ( emu, gsGeo ) ;

   if ( (getcwd ( cwd, PATH_MAX )) == NULL ) // get CWD
      cwd[0] = NULLCHAR ;

   //* Display captured and conditioned emulator data.*
   dumpEmuData ( emu, true, EName[cmdOption] ) ;

   // Command sequence can be enclosed in either single or double quotes.
   switch ( cmdOption )
   {
      case emuiGnome:      // gnome-terminal
         gsOut.compose( "--working-directory=%s", cwd ) ;
         gsOut.copy( cwd , PATH_MAX) ;
         gsCmd.compose( "%s -c \"grep -n 'COMPILE' Makefile ; exec %s\"",
                        emu.shName, emu.shName ) ;
         break ;

      case emuiKonsole:    // konsole
         {
         //* Geometry, if specified, must be separated into its component parts.*
         kwid[0] = khgt[0] = NULLCHAR ;   // initialize the buffers
         int pi = gsGeo.find( " -p" ) ;   // index the second element
         if ( pi > ZERO )
         {
            gsGeo.substr( kwid, ZERO, (pi <= 32 ? pi : 32) ) ;
            gsGeo.substr( khgt, ++pi, 32 ) ;
         }
         // else contents of gsGeo ignored

         gsOut.compose( "--workdir=%s", cwd ) ;
         gsOut.copy( cwd , PATH_MAX) ;
         gsCmd.compose( "%s -c \"grep -n 'COMPILE' Makefile ; exec %s\"",
                        emu.shName, emu.shName ) ;
         }
         break ;

      case emuiXTerm:      // xterm
         gsCmd.compose( "%s -c \"cd '$PWD' ; grep -n \'COMPILE\' Makefile ; exec %s\"",
                        emu.shName, emu.shName ) ;
         break ;

      case emuiUnknown:    // unsupported emulator
      default:
         break ;
   } ;

   //* If unsupported emulator, break the bad news and return. *
   if ( cmdOption == emuiUnknown )
   {
      wcout << L"  -------------------------------------\n"
                "  Sorry, unsupported terminal emulator.\n"
                "  -------------------------------------\n" << endl ;
      return ;    // note the early return
   }

   gsdbg.compose( "  >emulator: %s\n"
                  "  >geometry: %s\n"
                  "  >cwd     : %s\n"
                  "  >command : %s",
                  emu.emuName, gsGeo.ustr(), cwd, gsCmd.ustr() ) ;
   wcout << gsdbg << endl ;

   pid_t fpid = vfork () ;       // create the child process

   if ( fpid == ZERO )
   {  //* The child process (if created) executes here. *
      switch ( cmdOption )
      {
         case emuiGnome:         // gnome-terminal
            // Array Of Invocation Tokens for new tab in current window:
            // 1) emulator name (file to execute)
            // 2) emulator name (arg[0] names the executed file)
            // 3) specify new tab
            // 4) '-q' option specifies a "quiet" launch (no write to parent window)
            // 5) working directory path specification
            // 6) '-e' switch signal external command sequence
            // 7) concatenated sequence of external commands (see above)
            // 8) null pointer terminates the array
            if ( emu.emuTab )
            {
               execlp ( emu.emuName, emu.emuName, "--tab", "-q", cwd, 
                        "-e", gsCmd.ustr(), NULL ) ;
            }

            // Array Of Invocation Tokens for new window:
            // 1) emulator name (file to execute)
            // 2) emulator name (arg[0] names the executed file)
            // 3) specify new window
            // 4) '-q' option specifies a "quiet" launch (no write to parent window)
            // 5) no menubar for window with single tab
            // 6) window dimensions and position (position ignored)
            //    a) If geometry specified, then open window using values provided.
            //    b) Else if full-screen mode specified, open in full-screen mode.
            //    c) If no geometry specified, then use defaults (probably fullscreen mode)
            // 7) working directory path specification
            // 8) '-e' switch signal external command sequence
            // 9) concatenated sequence of external commands (see above)
            //10) null pointer terminates the array
            else if ( emu.wCols > ZERO )
            {     // open using specified geometry
               execlp ( emu.emuName, emu.emuName, "--window", "-q", "--hide-menubar", 
                              gsGeo.ustr(), cwd, "-e", gsCmd.ustr(), NULL ) ;
                              // Note: "-e" option is deprecated, but necessary. WTF?
            }
            else if ( emu.fullScr != false )
            {     // open window in full-screen mode
               execlp ( emu.emuName, emu.emuName, "--window", "-q", "--hide-menubar", 
                              "--full-screen", cwd, "-e", gsCmd.ustr(), NULL ) ;
                              // Note: "-e" option is deprecated, but necessary. WTF?
            }
            else  // (emu.dfltGeo != false)
            {     // open window using default geometry
               execlp ( emu.emuName, emu.emuName, "--window", "-q", "--hide-menubar", 
                              cwd, "-e", gsCmd.ustr(), NULL ) ;
                              // Note: "-e" option is deprecated, but necessary. WTF?
            }
            break ;
         case emuiKonsole:       // konsole
            // Array Of Invocation Tokens for new tab in current window:
            // 1) emulator name (file to execute)
            // 2) emulator name (arg[0] names the executed file)
            // 3) specify new tab (note: this is the default option)
            // 4) working directory path specification
            // 5) '-e' switch signal external command sequence
            // 6) concatenated sequence of external commands (see above)
            // 7) null pointer terminates the array
            if ( emu.emuTab )
            {
               execlp ( emu.emuName, emu.emuName, "--new-tab", cwd, 
                        "-e", gsCmd.ustr(), NULL ) ;
            }

            // Array Of Invocation Tokens:
            // 1) emulator name (file to execute)
            // 2) emulator name (arg[0] names the executed file)
            // 3) force same instance (silence konsole warning message)
            // 4) window width  (overrides current profile value)
            //    a) If emu.wCols > zero, then override profile width/height values.
            //    b) Else if full-screen mode specified, open in full-screen mode,
            //       and parameter #5 will be omitted.
            //    c) If no geometry specified, then use default profile dimensions.
            // 5) window height (overrides current profile value)
            // 6) working directory path specification
            // 7) '-e' switch signal external command sequence
            // 8) concatenated sequence of external commands (see above)
            // 9) null pointer terminates the array
            else if ( emu.wCols > ZERO )
            {     // open using specified geometry
               execlp ( emu.emuName, emu.emuName, "--force-reuse", 
                        kwid, khgt, cwd, 
                        "-e", gsCmd.ustr(), NULL ) ;
            }
            else if ( emu.fullScr != false )
            {     // open window in full-screen mode
               execlp ( emu.emuName, emu.emuName, "--force-reuse", 
                        "--fullscreen", cwd, 
                        "-e", gsCmd.ustr(), NULL ) ;
            }
            else  // (emul.dfltGeo != false)
            {     // open window using default geometry
               execlp ( emu.emuName, emu.emuName, "--force-reuse", 
                        cwd, "-e", gsCmd.ustr(), NULL ) ;
            }
            break ;
         case emuiXTerm:         // xterm
            // Array Of Invocation Tokens:
            // 1) emulator name (file to execute)
            // 2) emulator name (arg[0] names the executed file)
            // 3) TrueType font
            // 4) font family
            // 5) font size
            // 6) 12-point font
            // 7) geometry || fullscreen option
            //    a) If geometry option, then followed by dimensions and offset values
            //    b) If fullscreen option, then
            // 9) '-e' switch signal external command sequence
            //10) concatenated sequence of external commands (see above)
            //11) null pointer terminates the array
            if ( emu.wCols > ZERO )
            {     // open using specified geometry
               execlp ( emu.emuName, emu.emuName, "-fa", "Monospace", "-fs", "12", 
                        "-geometry", gsGeo.ustr(), 
                        "-e", gsCmd.ustr(), NULL ) ;
            }
            else if ( emu.fullScr != false )
            {     // open window in full-screen mode
               execlp ( emu.emuName, emu.emuName, "-fa", "Monospace", "-fs", "12", 
                        "-fullscreen", 
                        "-e", gsCmd.ustr(), NULL ) ;
            }
            else  // (emul.dfltGeo != false)
            {     // open window using default geometry
               execlp ( emu.emuName, emu.emuName, "-fa", "Monospace", "-fs", "12", 
                        "-e", gsCmd.ustr(), NULL ) ;
            }
            break ;
         case emuiUnknown:       // unsupported emulator (should never arrive here)
         default:
            break ;
      } ;

      wcout << " Error! Child process call to execlp() failed." << endl ;

      //* In case the exec call fails: child process MUST NOT continue.*
      _exit ( 0 ) ;
   }
   else
   {  //* The parent process continues execution here *
      //* Wait a moment (in case the child PID *
      //* writes an error message to display)  *
      chrono::duration<short, std::milli>aMoment( 250 ) ;
      this_thread::sleep_for( aMoment ) ;

      if ( fpid > ZERO )      // child successfully launched
      {
         gsOut.compose( " Mother salmon spawned child PID: %d"
                        " in a new %s.", &fpid, 
                        (emu.emuTab ? "tab" : "terminal window") ) ;
      }
      else
      {
         gsOut.compose( " Spawn failed: (%d), Mother salmon is childless.", &fpid ) ;
      }
      wcout << gsOut << endl ;
   }
}  //* End Test_Fork_execlp() *

//*************************
//*      getBasePath      *
//*************************
//********************************************************************************
//* Translate the invocation path/filename string to the full filespec of the    *
//* directory in which the application binarys live.                             *
//*                                                                              *
//* Input  : arg_zero : pointer to the app's invocation string i.e. argv[0]      *
//*                     argv[0]                                                  *
//*                                                                              *
//* Returns: 'true' if successful, or 'false' if system error (unlikely)         *
//********************************************************************************

bool Salmon::getBasePath ( const char* arg_zero )
{
   char fullPath[PATH_MAX] ;        // full path/filename
   gString fSpec ;
   int     indx ;
   bool status = false ;            // return value

   if ( (realpath ( arg_zero, fullPath )) != NULL )
   {
      fSpec = fullPath ;
      if ( (indx = fSpec.findlast( L'/' )) > ZERO )
         fSpec.limitChars( indx ) ;
      fSpec.copy( this->appPath, PATH_MAX ) ;      // save filespec of target directory
      fSpec.append( "/%s", childApp ) ;
      fSpec.copy( this->childFSpec, PATH_MAX ) ;   // save filespec of child application
      status = true ;
   }
   return status ;

}  //* End getBasePath() *

//*************************
//*    getTempfilePath    *
//*************************
//********************************************************************************
//* Query the system for the shared temp-file path.                              *
//* Save the pathspec to our member variable.                                    *
//*                                                                              *
//* The author's full-featured temporary file manager, TFMan.hpp does a more     *
//* robust verification, but the system "guarantees" that the path exists and    *
//* that it is a directory.                                                      *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: 'true' if successful, or 'false' if system error                    *
//********************************************************************************

bool Salmon::getTempfilePath ( void )
{
   bool status = false ;

   //* Get the filespec of the system temporary-file directory *
   std::filesystem::path p = std::filesystem::temp_directory_path () ;
   strncpy ( this->tfPath, p.c_str(), PATH_MAX ) ;

   return status ;

}  //* End getTempfilePath() *

//*************************
//*    createTempfile     *
//*************************
//********************************************************************************
//* Call the system to create a unique temporary file in the system shared       *
//* temporary directory.                                                         *
//*                                                                              *
//* Input  : tmpPath : (by reference) gString object to receive filespec         *
//* Returns: 'true'  if file successfully created                                *
//*                  file exists, and contains no data                           *
//*          'false' if library call failed                                      *
//********************************************************************************

bool Salmon::createTempfile ( gString& tmpPath )
{
   const char* tfTemplate = "%s/FISH_XXXXXX" ;  // temp filespec template

   //* If temp-file directory has not yet been identified *
   if ( *this->tfPath == NULLCHAR )
      this->getTempfilePath () ;

   char tn[PATH_MAX] ;                    // buffer to receive created temp filename
   gString gstmp( tfTemplate, this->tfPath ) ;
   gstmp.copy( tn, PATH_MAX ) ;
   int  descriptor ;                      // file descriptor for opened temp file
   bool status = false ;                  // return value

   tmpPath.clear() ;                      // initialize caller's buffer

   if ( (descriptor = mkstemp ( tn )) != (-1) ) // create the temp file
   {
      close ( descriptor ) ;              // close the file
      tmpPath = tn ;                      // copy target path to caller's buffer
      status = true ;                     // declare success
   }
   return status ;
}  //* End createTempfile() *

//*************************
//*    deleteTempfile     *
//*************************
//********************************************************************************
//* Delete a temporary file.                                                     *
//*                                                                              *
//* Input  : tmpPath : (by reference) filespec of file to be deleted             *
//*                                                                              *
//* Returns: 'true'  if file successfully deleted                                *
//*          'false' if library call failed (invalid filespec)                   *
//********************************************************************************

bool Salmon::deleteTempfile ( const gString& tmpPath )
{
   bool status = false ;
   if ( ((tmpPath.find( this->tfPath )) == 0) &&
        ((unlink ( tmpPath.ustr() )) == 0) )
      status = true ;
   return status ;

}  //* End deleteTempfile() *

//*************************
//*  DefineChildEmulator  *
//*************************
//********************************************************************************
//* Identify the terminal emulator program in which the application is running,  *
//* and if available, the name of the active shell program.                      *
//* Initialize all members of the emuData object for which data are available.   *
//*                                                                              *
//* In addition, if command line arguments have been specified, they will be     *
//* used to adjust the parameters as follows:                                    *
//*                                                                              *
//* NOTES:                                                                       *
//* ------                                                                       *
//* A) Up to four(4) arguments may be specified on the command line.             *
//*    If specified, these arguments are interpreted as follows:                 *
//*      1) "f" == fullscreen, or "t" == tab:                                    *
//*         a) If a numeric value, it is interpreted as the width of the child   *
//*            window in columns (range limited).                                *
//*         b) If "f", window will be opened in full-screen mode.                *
//*         c) If "t", instead of a new window, a new tab will be opened in      *
//*            the current window. (does not allpy to XTerm)                     *
//*         d) If none of the above, then arg 2 will be ignored.                 *
//*      2) If arg 1 is a numeric value:                                         *
//*         a) If arg 2 is a numeric value, then it will be interpreted as       *
//             the height of the child window in rows (range limited).           *
//*         b) I arg2 is not a numeric value, it will be ignored.                *
//*      3) If specified, the horizontal offset of the window in pixels.          *
//*         Note that only XTerm recognizes offsets. Konsole no longer recognizes*
//*         geometry values, and gnome-terminal simply ignores offsets,          *
//*      4) If specified, the vertical offset of the window in pixels.           *
//*         Note that only XTerm recognizes offsets. Konsole no longer recognizes*
//*         geometry values, and gnome-terminal simply ignores offsets,          *
//*                                                                              *
//*                                                                              *
//* B) If no command line arguments, or if the argument(s) are out of range,     *
//*    then window geometry will be set to a default size (see below).           *
//*    Wayland will center the window in the screen.                             *
//*                                                                              *
//* C) Note on offsets: A 12-point font is typically 16 pixels in height,        *
//*    and for the Courier monospace width-to-height ratio is approximately      *
//*    0.6 to 1. Each font of the monospace font group has its own ideas about   *
//*    this ratio.                                                               *
//*    The following reports the screen dimensions in pixels and millimeters,    *
//*    as well as the effective DPI: xdpyinfo | grep -A2 '^screen'               *
//*                                                                              *
//*                                                                              *
//* Technical Note:                                                              *
//* ---------------                                                              *
//* If the temporary-file manager has not yet been initialized, it will be       *
//* activated here.                                                              *
//*                                                                              *
//*                                                                              *
//* Input  : emu  : (by reference) emuData object to receive captured data       *
//*                     emulator version (see example)                           *
//*          gsGeo: (by reference) receives the geometry specification           *
//*                 constructed for the specific emulator type.                  *
//*                                                                              *
//* Returns: member of enum EmuIndex, indicates the identified terminal emulator *
//*          If current emulator not identified, or not supported, then          *
//*          'emuiUnknown' will be returned.                                     *
//********************************************************************************

EmuIndex Salmon::DefineChildEmulator ( emuData& emu, gString& gsGeo )
{
   const int dfltWID = 65 ;
   const int dfltHGT = 8 ;
   gString tmpSpec ;
   gString gsEmu ;
   gString gs ;
   int nparm ;
   EmuIndex emui = emuiUnknown ;          // return value

   gsGeo.clear() ;      // initialize caller's geometry buffer

   //* If the system's temp-file directory has not yet been identified *
   //* do it now. Then create two temporary files.                     *
   if ( *this->tfPath == NULLCHAR )
      this->getTempfilePath () ;

   //* Create a temporary file to capture the raw data. *
   this->createTempfile ( tmpSpec ) ;

   //* Capture and decode the information *
   emui = this->getEmulator ( gsEmu, tmpSpec.ustr() ) ;
   emu.decode( gsEmu ) ;

   //* Initialize default window size/position. *
   emu.wCols = dfltWID ;
   emu.wRows = dfltHGT ;
   emu.xOff  = -1 ;
   emu.yOff  = -1 ;

   //* If alternate geometry specified as command line arguments *
   if ( this->argCnt > 2 )
   {
      gs = this->argList[2] ;
      if ( gs.gstr()[0] == L'f' )
      { emu.fullScr = true ; emu.wCols = emu.wRows = emu.xOff = emu.yOff = ZERO ; }
      else if  ( (gs.gstr()[0] == L't') && (emui != emuiXTerm) )
      { emu.emuTab = true ; emu.wCols = emu.wRows = emu.xOff = emu.yOff = ZERO ; }
      else if ( (gs.gscanf( "%d", &nparm )) == 1 )
      {
         if ( nparm > ZERO )
            emu.wCols = nparm ;
         else if ( nparm == ZERO )
         { emu.fullScr = true ; emu.wCols = emu.wRows = emu.xOff = emu.yOff = ZERO ; }
         else     // negative value (indicates default geometry)
         { emu.dfltGeo = true ; emu.wCols = emu.wRows = emu.xOff = emu.yOff = ZERO ; }

         if ( (this->argCnt > 3) && (emu.wCols > ZERO) )
         {
            gs = this->argList[3] ;
            if ( (gs.gscanf( "%d", &nparm )) == 1 )
            {
               if ( nparm > ZERO )
                  emu.wRows = nparm ;
               // ignore value <= ZERO
            }

            //* If parent emulator is xterm, then horizontal and vertical *
            //* offsets are recognized. 
            if ( this->argCnt > 4 )
            {
               gs = this->argList[4] ;
               if ( (gs.gscanf( "%d", &nparm )) == 1 )
               {
                  if ( nparm > ZERO )
                  {
                     emu.xOff = nparm ;
                     if ( this->argCnt > 5 )
                     {
                        gs = this->argList[5] ;
                        if ( (gs.gscanf( "%d", &nparm )) == 1 )
                        {
                           if ( nparm > ZERO )
                              emu.yOff = nparm ;
                        }
                     }
                  }
                  // ignore value <= ZERO
               }
            }
         }
      }
   }

   switch ( emui )
   {
      case emuiGnome:
         if ( emu.xOff < ZERO ) { emu.xOff = ZERO ; } // normalize (ignored by gnome-terminal)
         if ( emu.yOff < ZERO ) { emu.yOff = ZERO ; }
         if ( !emu.emuTab && emu.wCols > ZERO )       // child window geometry
            gsGeo.compose( "--geometry=%dx%d+%d+%d", 
                           &emu.wCols, &emu.wRows, &emu.xOff, &emu.yOff ) ;
         break ;

      case emuiKonsole:
         if ( emu.xOff < ZERO ) { emu.xOff = ZERO ; } // normalize (ignored by konsole)
         if ( emu.yOff < ZERO ) { emu.yOff = ZERO ; }
         if ( !emu.emuTab && emu.wCols > ZERO )       // child window geometry
            gsGeo.compose( "-p TerminalColumns=%d -p TerminalRows=%d",
                           &emu.wCols, &emu.wRows ) ;
         break ;

      case emuiXTerm:
         if ( emu.wCols > ZERO )       // child window geometry
         {
            if ( (emu.xOff < ZERO) || (emu.yOff < ZERO) )   // default offsets
            {
               gsGeo.compose( "%dx%d", &emu.wCols, &emu.wRows ) ;
               emu.xOff = emu.yOff = ZERO ;
            }
            else                       // both dimensions and offsets specified
            {
               gsGeo.compose( "%dx%d+%d+%d",
                              &emu.wCols, &emu.wRows, &emu.xOff, &emu.yOff ) ;
            }
         }
         break ;

      default:                         // unknown emulator
         break ;
   } ;

   this->deleteTempfile ( tmpSpec ) ;     // delete the temporary file
   return emui ;

}  //* End DefineChildEmulator() *

//*************************
//*      getEmulator      *
//*************************
//********************************************************************************
//* Traverse the parent PID records until the terminal emulator name is found.   *
//* or until the top of the process tree is reached.                             *
//* 1) If a supported terminal emulator application is identified, then its      *
//*    name and process ID are reported.                                         *
//* 2) If available, the emulator version number is reported.                    *
//* 3) If available, the name of the shell program, its process ID and version   *
//*    number are reported.                                                      *
//*    Note: If a supported emulator is identified, the it is ASSUMED that       *
//*          the name of the shell program name is the name of the emulator's    *
//*          child process. This is logically true, but there may be exceptions. *
//*                                                                              *
//* Input  : gsEmu    : (by reference) receives name of terminal emulator and    *
//*                     other information.                                       *
//*          tmpSpec  : filespec of a temporary file to be used in gathering     *
//*                     the requested information.                               *
//*                                                                              *
//* Returns: member of enum EmuIndex, indicates the identified terminal emulator *
//*          If current emulator not identified, or not supported, then          *
//*          'emuiUnknown' will be returned.                                     *
//*          If unable to determine emulator name, geEmu will contain an         *
//*          empty string.                                                       *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* 1) Only the top three terminal emulators are supported by this scan:         *
//*      a) gnome-terminal                                                       *
//*      b) Konsole                                                              *
//*      c) xTerm                                                                *
//*    In the AnsiCmd Library version of this method, three additional           *
//*    emulators are supported:                                                  *
//*      d) Tilda                                                                *
//*      e) Terminator                                                           *
//*      f) Yakuake                                                              *
//* 2) Five shell programs are recognized by this scan:                          *
//*      a) bash                                                                 *
//*      b) zsh                                                                  *
//*      c) fish                                                                 *
//*      d) ksh                                                                  *
//*      e) sh                                                                   *
//*    The Debian Almquist Shell (dash) does not implement a --version (-v)      *
//*    option; therefore, it is excluded from the list.                          *
//*    Both "csh" and "tchs" are functionally obsolete and therefore are not     *
//*    supported here.                                                           *
//* 3) A maximum of five (5) branches of the process ID tree are scanned;        *
//*    however, we have never seen an emulator at a depth greater than three,    *
//*    so five seems like a safe limit.                                          *
//*                                                                              *
//********************************************************************************
//* The interesting data are in (at least) the following file:                   *
//*      '/proc/PROCNUM/stat'                                                    *
//* The 'stat' file's first line contains a bunch of stuff, but it is the        *
//* second (2nd) and fourth (4th) tokens that are of interest:                   *
//* token #1: this is the process ID for the current file.                       *
//* token #2: this is the name of the the process (enclosed in parentheses)      *
//*           Note that this may be some variation of the actual name; however,  *
//*           it is the canonical application name that is returned.             *
//* token #3: this is the letter 'S' or 'R'. and we currently don't care         *
//*           what that means.                                                   *
//* token #4: this is the parent process's procID.                               *
//*                                                                              *
//*                                                                              *
//* If the emulator name is identified, then at least two(2) substring values    *
//* are returned, separated by a colon token (" :").                             *
//*    Example: "gnome-terminal :4674"                                           *
//*             |               |....| process ID                                *
//*             |.............. | emulator name                                  *
//* If additional data are captured, each additional token will be appended,     *
//* separated by colon tokens.                                                   *
//*    "gnome-terminal :4674 :3.56.3 :bash :4682 :5.2.37"                        *
//*    |               |     |       |     |     |...... | shell version number  *
//*    |               |     |       |     |.... | shell process ID              *
//*    |               |     |       |.... | shell program                       *
//*    |               |     |...... | emulator version number                   *
//*    |               |.... | process ID                                        *
//*    |.............. emulator name                                             *
//*                                                                              *
//* This string can then be decoded using the emuData-class decode() method.     *
//*                                                                              *
//* For unknown substring values, the noJoy constant will be substituted:        *
//* (If emulator is not identified, then PID values will be zero.)               *
//*    "unknown :0 :unknown :unknown :0 :unknown"                                *
//*    "gnome-terminal :4674 :unknown :bash :4682 :5.2.37"                       *
//*    "gnome-terminal :4674 :3.56.3 :bash :4682 :unknown"                       *
//* If an emulator is identified, but the shell name is not one of the supported *
//* shells, then the shell name will be returned, but the version number cannot  *
//* be captured, so it will be reported as the noJoy constant:                   *
//*    "gnome-terminal :4674 :3.56.3 :tortise_shell :4682 :unknown"              *
//*                                                                              *
//********************************************************************************

EmuIndex Salmon::getEmulator ( gString& gsEmu, const char* tmpSpec )
{
   const short enMAX = 3 ;
   // Programmer's Note: The layout of this array matches enum EmuIndex.
   const char* emuNames[enMAX]
   {
      "gnome-terminal",
      "konsole",
      "xterm",
   } ;
   const char* emuVers[enMAX]
   {
      "gnome-terminal --version",
      "konsole --version",
      "xterm -version",
   } ;
   const short shMAX = 5 ;
   const char* shNames[shMAX]
   {
      "bash",           // Bourne Again Shell
      "zsh",            // Z Shell
      "fish",           // Friendly Interactive Shell
      "ksh",            // Korn Shell
      "sh",             // Shell (usually an alias for 'bash')
   } ;
   const char* shVers[shMAX]
   {
      "bash --version",
      "zsh --version",
      "fish --version", // (also 'fish -v')
      "ksh --version",
      "sh --version",
   } ;
   const char* noJoy = "unknown" ;
   const char* scanTemplate = "%d %s %s %d" ;
   gString gsSpec, gsVer, gs ;
   const short BRANCHES = 5 ;
   pid_t pid = getppid (),    // PARENT process ID 
         spid, ppid, prevPid ;
   char pname[64], prevName[64], toss[64] ;
   int indx ;
   EmuIndex emui = emuiUnknown ;          // return value
   bool status = false ;

   gsEmu.clear() ;            // initialize caller's object

   //* Scan the tree for the emulator name *
   for ( short p = BRANCHES ; (p > ZERO) ; --p )
   {
      gsSpec.compose( "/proc/%d/stat", &pid ) ;  // file containing parent process
      if ( (extractFirstLine ( gsSpec.ustr(), gs )) )
      {
         //* Extract the first four tokens from the source data.        *
         //* Ensure that the PID extracted matches the expected value.  *
         //* Note that a ppid==1 indicates the top of the process tree, *
         //* so if seen, we have failed. (unlikely)                     *
         if ( ((gs.gscanf( scanTemplate, &spid, pname, toss, &ppid )) == 4)
              && (spid == pid) && (ppid > 1) )
         {
            //* Strip the '()' from 'pname' , then *
            //* test for something that looks like *
            //* the name of a terminal emulator.   *
            gs = pname ;
            if ( (indx = gs.find( L'(')) >= ZERO )
               gs.erase( indx, 1 ) ;
            if ( (indx = gs.find( L')')) >= ZERO )
               gs.erase( indx, 1 ) ;
            gs.copy( pname, 64 ) ;
            for ( indx = ZERO ; indx < enMAX ; ++indx )
            { if ( (gs.find( emuNames[indx] )) >= ZERO ) { break ; } }

            //* If a matching emulator name, save name and PID. *
            if ( indx < enMAX )
            {
               gsEmu.compose( "%s :%d", emuNames[indx], &pid ) ;
               switch ( indx )
               {
                  case 0:  emui = emuiGnome ;      break ;
                  case 1:  emui = emuiKonsole ;    break ;
                  case 2:  emui = emuiXTerm ;      break ;
               } ;
               //* The name of the shell program should be in prev[].*
               status = true ;      // success
               break ;
            }

            else                    // save most recent non-match
            {
               gs.copy( prevName, 64 ) ;
               prevPid = spid ;
            }

            pid = ppid ;            // move to the next higher level
         }
         else { break ; }
      }
      else { break ; }
   }

   //* If capture of emulator name was successful,   *
   //* query emulator program for its version number.*
   if ( status )
   {
      const char* cmdTemplate =           // command template
               "%s 1>\"%s\" 2>/dev/null" ;

      //* Invoke the application, requesting version number. *
      //* Extract the version number from the file.          *
      gsSpec.compose( cmdTemplate, emuVers[indx], tmpSpec ) ;
      system ( gsSpec.ustr() ) ;
      if ( (extractFirstLine ( tmpSpec, gs )) )
      {
         extractNumericSubstring ( gs, gsVer ) ;
         gsEmu.append( " :%S", gsVer.gstr() ) ;
      }
      else { gsEmu.append( " :%s", noJoy ) ; }

      //* Verify that the shell program is one of supported names.*
      gs = prevName ;
      for ( indx = ZERO ; indx < shMAX ; ++indx )
      { if ( (gs.find( shNames[indx] )) >= ZERO ) { break ; } }
      //* Invoke the shell program, requesting version number. *
      //* Extract the version number from the file.            *
      if ( indx < shMAX )
      {
         gsSpec.compose( cmdTemplate, shVers[indx], tmpSpec ) ;
         system ( gsSpec.ustr() ) ;
         if ( (extractFirstLine ( tmpSpec, gs )) )
            extractNumericSubstring ( gs, gsVer ) ;
         else
            gsVer = noJoy ;
      }
      else
         gsVer = noJoy ;

      //* Append shell program name *
      gsEmu.append( " :%s :%d :%S", prevName, &prevPid, gsVer.gstr() ) ;
   }
   else     // no emulator match, initialize default string
      gsEmu.compose( "%s :0 :%s :%s :0 :%s", noJoy, noJoy, noJoy, noJoy ) ;

   return emui ;

}  //* End getEmulator() *

//***************************
//*       dumpEmuData       *
//***************************
//********************************************************************************
//* Non-member method:                                                           *
//* ------------------                                                           *
//* Input  : emu    : (by reference) EmuData object containing data for display  *
//*          verbose: (optional, default:false                                   *
//*                   if 'true',  display verbose output                         *
//*                   if 'false', display basic data only                        *
//*          msg    : (optional, default: null pointer)                          *
//*                   if 'true',  message to be written at end of first display  *
//*                               line (typically, text of enum EmuIndex)        *
//*                                                                              *
//* Returns: 'true'  nothing                                                     *
//********************************************************************************

static void dumpEmuData ( const emuData& emu, bool verbose, const char* msg )
{
   const char* msgPtr = " " ;
   if ( msg != NULL )
      msgPtr = msg ;
   gString gs( 
              " emuData: %s\n"
              "  emuName   :%s\n"
              "  emuVersion:%s\n"
              "  emuPid    :%d\n"
              "  shName    :%s\n"
              "  shVersion :%s\n"
              "  shPid     :%d\n",
              msgPtr,
              emu.emuName, emu.emuVersion, &emu.emuPid,
              emu.shName,  emu.shVersion,  &emu.shPid ) ;
   wcout << gs ; wcout.flush() ;
   if ( verbose )
   {
      gs.compose( 
              "  wCols:%d wRows:%d xOff:%d yOff:%d fullScr:%s dfltGeo:%s emuTab:%s",
              &emu.wCols, &emu.wRows, &emu.xOff, &emu.yOff,
              (emu.fullScr ? "true" : "false"),
              (emu.dfltGeo ? "true" : "false"),
              (emu.emuTab  ? "true" : "false") ) ;
      wcout << gs << endl ;
   }

}  //* End dumpEmuData() *

//***************************
//*    extractFirstLine     *
//***************************
//********************************************************************************
//* Non-member method:                                                           *
//* ------------------                                                           *
//* Input  : src    : filespec for source data                                   *
//*          trg    : pointer to buffer to receive the extracted text            *
//*          trg_max: size of 'trg' buffer (bytes)                               *
//*                                                                              *
//* Returns: 'true'  if data successfully extracted                              *
//*          'false' if no data extracted (trg == empty string)                  *
//********************************************************************************

static bool extractFirstLine ( const char* src, gString& trg )
{
   const int ROW_MAX = 256 ;
   ifstream ifs( src, ifstream::in ) ;    // open the source file
   char raw[ROW_MAX] ;
   bool status = false ;                  // return value
   trg.clear() ;                          // initialize target buffer

   if ( ifs.is_open() )
   {
      ifs.getline ( raw, ROW_MAX, NEWLINE ) ; // get the first line of the file
      if ( (ifs.good() || ifs.gcount() > ZERO) )
      {
         trg = raw ;
         status = true ;
      }
      ifs.close() ;                       // close the file
   }
   return status ;

}  //* End extractFirstLine() *

//***************************
//* extractNumericSubstring *
//***************************
//********************************************************************************
//* Non-member method:                                                           *
//* ------------------                                                           *
//* Given a source string, extract the first numeric substring.                  *
//* Valid numeric characters are '0' - '9' and '.' (full stop)                   *
//* If a nummeric substring is not found, the string "unknown" will be returned. *
//*                                                                              *
//* This method is designed to extract the version number from a captured        *
//* text string, but may have other uses.                                        *
//*                                                                              *
//* Input  : src  : (by reference) source string data                            *
//*          trg  : pointer to awchar_t buffer to receive the substring          *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

static void extractNumericSubstring ( const gString& src, gString& trg )
{
   const wchar_t* noJoy = L"unknown" ;
   const wchar_t* wptr = src.gstr() ;
   wchar_t wbuff[64] ;
   int   eof = src.gschars(), si = ZERO, ti = ZERO ;

   while ( (si < eof) && 
           !((wptr[si] >= L'0') && (wptr[si] <= L'9')) )
   { ++si ; }
   if ( si < eof )
   {
      while ( (si < eof) && //(ti < trglen) &&
              (((wptr[si] >= L'0') && (wptr[si] <= L'9')) || (wptr[si] == L'.')) )
      { wbuff[ti++] = wptr[si++] ; }
      wbuff[ti] = NULLCHAR ;
      trg = wbuff ;
   }
   else
      trg = noJoy ;

}  //* End extractNumericSubstring() *

//*************************
//*        Sandbox        *
//*************************
//********************************************************************************
//* For debugging only: Execute experimental commands.                           *
//*                                                                              *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* -- If this->argList[2] == "dump", report emulator/shell information.         *
//* -- If this->argList[2] == "screen", report dimensions of computer screen.    *
//*                                                                              *
//* Misc. Test and information gathering used during development:                *
//* -------------------------------------------------------------                *
//* system ( "loginctl show-session $(loginctl | grep $(whoami)                  *
//*           | awk '{print $1}') -p Type --value" ) ;                           *
//* system ( "loginctl show-session $(awk '/tty/ {print $1}'                     *
//*           <(loginctl)) -p Type | awk -F= '{print $2}'" ) ;                   *
//* system ( "ps -o 'ppid=' -p $$" ) ;                                           *
//* system ( "ps -o 'cmd=' -p $(ps -o 'ppid=' -p $$)" ) ;                        *
//* system ( "ps -o 'cmd=' -p $(ps -o 'ppid=' -p $(ps -o 'ppid=' -p $$))" ) ;    *
//* system ( "ps -o 'cmd=' -p $(ps -o 'ppid=' -p $(ps -o 'ppid=' -p $(ps -o      *
//*          'ppid=' -p $$)))" ) ;                                               *
//* system ( "echo $WINDOWID" ) ;                                                *
//* system ( "xwininfo -id $WINDOWID -wm | grep 'Process id:'" ) ;               *
//* system ( "xwininfo -wm -id $WINDOWID | grep 'Process id:'                    *
//*          | grep -o '[[:digit:]]*' | grep '[[:digit:]]' --max-count=1" ) ;    *
//* system ( "ps -o 'cmd=' -p $(xwininfo -wm -id $WINDOWID | grep 'Process id:'  *
//*          | grep -o '[[:digit:]]*' | grep '[[:digit:]]' --max-count=1)" ) ;   *
//* system ( "xdpyinfo | grep -A2 '^screen'" ) ;                                 *
//* system ( "xterm -geometry 65x10+400+300 -fn fixed -fs 16                     *
//*          -e \"bash -c grep --version\" ; exec bash" ) ;                      *
//* execlp ( "xterm", "xterm", "-fn", "fixed", "-fs", "18",                      *
//*          "-geometry", "80x16+550+350", NULL ) ;                              *
//* execlp ( "xterm", "xterm", "-fn", "fixed", "-fs", "9x18",                    *
//*          "-geometry", "80x8+550+350", NULL ) ;                               *
//* execlp ( "gnome-terminal", "gnome-terminal", "--geometry=65x9+400+300",      *
//*          "--hide-menubar", "-e", "bash -c                                    *
//*          \"grep -n \'COMPILE\' Makefile MakeChild ; exec bash\"", NULL ) ;   *
//* execlp ( "xterm", "xterm", "-fa", "Monospace", "-fs", "12",                  *
//*          "-geometry", "80x8+550+350",                                        *
//*          "-e", "bash -c \"gcc --version ; exec bash\"", NULL ) ;             *
//* execlp ( "konsole", "konsole", "--geometry=1200x800+550+350",                *
//*          "--hide-menubar",                                                   *
//*          "-e", "bash -c \"gcc --version ; exec bash\"", NULL ) ;             *
//********************************************************************************

void Salmon::Sandbox ( void )
{
   //* This special case reports emuData emulator/shell information.*
   if ( this->argCnt > 2 )
      gsdbg = this->argList[2] ;
   if ( (gsdbg.compare( L"dump" )) == ZERO )
   {
      emuData emu ;
      gString gs, tempSpec ;
      this->createTempfile ( tempSpec ) ;
      this->getEmulator ( gsdbg, tempSpec.ustr() ) ;
      emu.decode ( gsdbg ) ;
      dumpEmuData ( emu, false ) ;
      this->deleteTempfile ( tempSpec ) ;
      wcout << endl ;
      return ;
   }
   //* This special case reports the dimensions of the computer screen.*
   else if ( (gsdbg.compare( L"screen" )) == ZERO )
   {
      gString tempSpec ;
      this->createTempfile ( tempSpec ) ;
      sboxScreenReport ( tempSpec.ustr() ) ;
      this->deleteTempfile ( tempSpec ) ;
      wcout << endl ;
      return ;
   }

   //* Execute experimental commands at the command prompt. *
   wcout << L"Experimental Commands:" << endl ;






}  //* End Sandbox() *

//*************************
//*   sboxScreenReport    *
//*************************
//********************************************************************************
//* Non-member Method:                                                           *
//* ------------------                                                           *
//* Capture and report dimensions of computer screen. Called from Sandbox().     *
//*                                                                              *
//* Raw data should look something like this:                                    *
//* screen #0:                                                                   *
//*   dimensions:    1920x1080 pixels (508x286 millimeters)                      *
//*   resolution:    96x96 dots per inch                                         *
//*                                                                              *
//* Input  : tempSpec : filespec of temporary file for data capture              *
//*                                                                              *
//* Returns: 'true' if all expected data captured successfully                   *
//*          'false' if some or all data did not comply with expected format     *
//********************************************************************************

static void sboxScreenReport ( const char* tempSpec )
{
   const wchar_t* capError = L" Error capturing screen info." ;
   const int ROW_MAX = 256 ;
   char raw[ROW_MAX] ;
   gString gs ;
   int scrnum ;
   bool status = false ;

   gs.compose( "xdpyinfo | grep -A2 '^screen' 1>'%s' 2>&1", tempSpec ) ;
   system ( gs.ustr() ) ;

   ifstream ifs( tempSpec, ifstream::in ) ;
   if ( ifs.is_open() )
   {
      ifs.getline ( raw, ROW_MAX, NEWLINE ) ; // get the first line of the file
      if ( (ifs.good() || ifs.gcount() > ZERO) )
      {
         //* Verify syntax of captured data *
         gs = raw ;
         if ( (gs.gscanf( "screen #%d", &scrnum )) == 1 )
         {
            gsdbg.compose( " Dimensions of Screen #%d:\n", &scrnum ) ;
            wcout << gsdbg ; wcout.flush() ;
            status = true ;

            ifs.getline ( raw, ROW_MAX, NEWLINE ) ;
            if ( (ifs.good() || ifs.gcount() > ZERO) )
            {
               gs = raw ;
               gs.strip() ;
               gs.replace( L"    ", L" " ) ;
               if ( (gs.find( "dimensions:" )) == ZERO )
               {
                  wcout << L"  " << gs << L"\n" ; wcout.flush() ;

                  ifs.getline ( raw, ROW_MAX, NEWLINE ) ;
                  if ( (ifs.good() || ifs.gcount() > ZERO) )
                  {
                     gs = raw ;
                     gs.strip() ;
                     gs.replace( L"    ", L" " ) ;
                     if ( (gs.find( "resolution:" )) == ZERO )
                     { wcout << L"  " << gs << L"\n" ; wcout.flush() ; }
                     else { status = false ; }
                  }
                  else { status = false ; }
               }
               else { status = false ; }
            }
            else { status = false ; }
         }
      }
      ifs.close() ;                    // close the file
   }

   if ( ! status )
      wcout << capError << endl ;

}  //* End sboxScreenReport() *

//*************************
//*     reportVersion     *
//*************************
//********************************************************************************
//* Display application version number and copyright info.                       *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//*******************************************************************************

void Salmon::reportVersion ( void )
{
   static const char* const freeSoftware = 
   "License GPLv3+: GNU GPL version 3 <http://gnu.org/licenses/gpl.html>\n"
   "This is free software: you are free to modify and/or redistribute it\n"
   "under the terms set out in the license.\n"
   "There is NO WARRANTY, to the extent permitted by law.\n" ;

   gsdbg.compose( "Salmon v:%s Copyright %s The Software Samurai\n", 
                  AppVersion, AppYears ) ;
   gsdbg.padCols( (gsdbg.gscols() * 2), L'=' ) ;
   gsdbg.append( "\n%s", freeSoftware ) ;
   wcout << gsdbg << endl ;

}  //* End reportVersion() *

//*************************
//*       helpMe          *
//*************************
//********************************************************************************
//* Display application command-line help.                                       *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//*******************************************************************************

void Salmon::helpMe ( void )
{
   wcout << "Salmon v:" << AppVersion 
         << " Copyright " << AppYears << " The Software Samurai\n"
              "========================================================\n"
              "Invoke: salmon -[v|l|p|f|s|t|D] [ARG1[ ARG2[ ARG3[ ARG4]]]]\n"
              "   v  execv()  pass an argv[] array to childApp\n"
              "               (calling application terminates)\n"
              "   l  execl()  pass individual arguments to childApp\n"
              "               (calling application terminates)\n"
              "   p  execlp() pass individual arguments to grep utility\n"
              "               Note that 'grep' utility is on the system PATH.\n"
              "               (calling application terminates)\n"
              "   f  execl()  'fork' a child process and pass individual arguments to childApp\n"
              "               (wait for child process to terminate)\n"
              "   s  execvp() 'fork' a child process to call childApp with stdout and stderr\n"
              "               redirected to sout.txt and serr.txt, respectively.\n"
              "               (calling application continues immediately)\n"
              "   t  execlp() 'fork' a child process to open a new terminal window,\n"
              "               which executes the 'grep' utility.\n"
              "               (calling application continues immediately)\n"
              "   D           Sandbox area for debugging and experimentation.\n"
         << endl ;

}  //* End helpMe()) *

//*************************
//*                       *
//*************************
//********************************************************************************
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* Input  :                                                                     *
//*                                                                              *
//* Returns:                                                                     *
//********************************************************************************

