i/o for parent child good, but some fd is staying open

master
Chris Punches 2021-03-07 23:29:47 -05:00
parent d4a228f351
commit 31ed2feb7f
4 changed files with 292 additions and 60 deletions

View File

@ -1,10 +1,16 @@
#include "Sproc.h"
#include <unistd.h>
#include <sys/wait.h>
#include <pwd.h>
#include <grp.h>
#include <fstream>
#include "../Logger/Logger.h"
#include "errno.h"
#include <cstring>
#include "fcntl.h"
// converts username to UID
// returns false on failure
int username_to_uid( std::string username, int & uid )
{
// assume failure unless proven otherwise
@ -22,6 +28,8 @@ int username_to_uid( std::string username, int & uid )
return r_code;
};
// converts group name to GID
// returns false on failure
int groupname_to_gid( std::string groupname, int & gid )
{
int r_code = false;
@ -37,77 +45,266 @@ int groupname_to_gid( std::string groupname, int & gid )
return r_code;
}
// teebuf constructor
teebuf::teebuf(std::streambuf *sb1, std::streambuf *sb2): sb1(sb1), sb2(sb2)
{}
// teebuf overflow method
int teebuf::overflow( int c )
{
if (c == EOF)
{
return !EOF;
}
else
{
int const r1 = sb1->sputc(c);
int const r2 = sb2->sputc(c);
return r1 == EOF || r2 == EOF ? EOF : c;
}
}
// teebuf sync method
int teebuf::sync()
{
int const r1 = sb1->pubsync();
int const r2 = sb2->pubsync();
return r1 == 0 && r2 == 0 ? 0 : -1;
}
// teestream impl constructor
teestream::teestream(std::ostream &o1, std::ostream &o2) : std::ostream( &tbuf), tbuf( o1.rdbuf(), o2.rdbuf() )
{}
enum PIPE_FILE_DESCRIPTORS {
READ_FD = 0,
WRITE_FD = 1
};
enum SPROC_RETURN_CODES {
SUCCESS = true,
UID_NOT_FOUND = -404,
GID_NOT_FOUND = -405,
SET_GID_FAILED = -401,
SET_UID_FAILED = -404,
EXEC_FAILURE_GENERAL = -666,
DUP2_FAILED = -999,
PIPE_FAILED = -998
};
enum FORK_STATES {
FORK_FAILURE = -1,
CHILD = 0
};
int set_identity_context( std::string task_name, std::string user_name, std::string group_name, Logger slog ) {
// the UID and GID for the username and groupname provided for context setting
int context_user_id;
int context_group_id;
// show the user something usable in debug mode
slog.log( E_DEBUG, "[ '" + task_name + "' ] Attempt: Running as user '" + user_name + "'.");
slog.log( E_DEBUG, "[ '" + task_name + "' ] Attempt: Running as group_name '" + group_name + "'.");
// convert username to UID
if ( username_to_uid(user_name, context_user_id ) )
{
slog.log( E_DEBUG, "[ '" + task_name + "' ] UID of '" + user_name + "' is '" + std::to_string(context_user_id ) + "'." );
} else {
slog.log( E_FATAL, "[ '" + task_name + "' ] Failed to look up UID for '" + user_name + "'.");
return SPROC_RETURN_CODES::UID_NOT_FOUND;
}
// convert group name to GID
if ( groupname_to_gid(group_name, context_group_id ) )
{
slog.log( E_DEBUG, "[ '" + task_name + "' ] GID of '" + group_name + "' is '" + std::to_string(context_group_id ) + "'." );
} else {
slog.log( E_FATAL, "[ '" + task_name + "' ] Failed to look up GID for '" + group_name + "'.");
return SPROC_RETURN_CODES::GID_NOT_FOUND;
}
if (setgid(context_group_id) == 0) {
slog.log(E_DEBUG,
"[ '" + task_name + "' ] Successfully set GID to '" + std::to_string(context_group_id) + "' (" +
group_name + ").");
} else {
slog.log(E_FATAL, "[ '" + task_name + "' ] Failed to set GID. Panicking.");
return SPROC_RETURN_CODES::SET_GID_FAILED;
}
if (setuid(context_user_id) == 0) {
slog.log(E_DEBUG,
"[ '" + task_name + "' ] Successfully set UID to '" + std::to_string(context_user_id) + "' (" +
user_name + ").");
} else {
slog.log(E_FATAL, "[ '" + task_name + "' ] Failed to set UID. Panicking.");
return SPROC_RETURN_CODES::SET_UID_FAILED;
}
return SPROC_RETURN_CODES::SUCCESS;
}
/// Sproc::execute
///
/// \param input - The commandline input to execute.
/// \return - The return code of the execution of input in the calling shell.
int Sproc::execute( std::string shell, std::string environment_file, std::string run_as, std::string group, std::string command, int LOG_LEVEL, std::string task_name )
int Sproc::execute(std::string shell, std::string environment_file, std::string user_name, std::string group_name, std::string command, int LOG_LEVEL, std::string task_name )
{
// the logger
Logger slog = Logger( LOG_LEVEL, "_sproc" );
// the run_as_uid to capture the run_as_uid to run as
int run_as_uid;
int run_as_gid;
slog.log( E_DEBUG, "[ '" + task_name + "' ] Attempt: Running as user '" + run_as + "'.");
slog.log( E_DEBUG, "[ '" + task_name + "' ] Attempt: Running as group '" + group + "'.");
if ( username_to_uid( run_as, run_as_uid ) )
{
slog.log( E_DEBUG, "[ '" + task_name + "' ] UID of '" + run_as + "' is '" + std::to_string( run_as_uid ) + "'." );
} else {
slog.log( E_FATAL, "[ '" + task_name + "' ] Failed to look up UID for '" + run_as + "'.");
return -404;
}
if ( groupname_to_gid( group, run_as_gid ) )
{
slog.log( E_DEBUG, "[ '" + task_name + "' ] GID of '" + group + "' is '" + std::to_string( run_as_gid ) + "'." );
} else {
slog.log( E_FATAL, "[ '" + task_name + "' ] Failed to look up GID for '" + group + "'.");
return -404;
}
// if you get this return value, it's an issue with this method and not your
// called executable.
int exit_code_raw = -666;
int exit_code_raw = SPROC_RETURN_CODES::EXEC_FAILURE_GENERAL;
// fork a process
int pid = fork();
// An explanation is due here:
// We want to log the STDOUT and STDERR of the child process, while still displaying them in the parent, in a way
// that does not interfere with, for example libcurses compatibility.
if ( pid == 0 )
{
// child process
if ( setgid( run_as_gid ) == 0 )
{
slog.log( E_DEBUG, "[ '" + task_name + "' ] Successfully set GID to '" + std::to_string(run_as_gid) + "' (" + group + ")." );
} else {
slog.log( E_FATAL, "[ '" + task_name + "' ] Failed to set GID. Panicking." );
return -401;
}
// To simplify the handling of I/O, we will "tee" STDOUT and STDERR of the parent to respective log files.
if ( setuid( run_as_uid ) == 0 )
{
slog.log( E_DEBUG, "[ '" + task_name + "' ] Successfully set UID to '" + std::to_string(run_as_uid) + "' (" + run_as + ")." );
} else {
slog.log( E_FATAL, "[ '" + task_name + "' ] Failed to set UID. Panicking." );
return -401;
}
// Then fork(), and exec() the command to execute in the child, and link its STDOUT/STDERR to the parents' in
// realtime.
std::string sourcer = shell + " -c \". " + environment_file + " && " + command + "\"";
// Since the parent has a Tee between STDOUT/STDOUT_LOG and another between STDERR/STDERR_LOG, simply piping the
// child STDOUT/STDERR to the parent STDOUT/STDERR should simplify I/O redirection happening here without
// potentially corrupting user interaction with TUIs in the processes. This should give us our log and our output
// in as hands off a way as possible with as few assumptions as possible, while still doing this in a somewhat C++-y
// way.
// I have no idea why this never shows up in the output!
// set up the "Tee" with the parent
std::string child_stdout_log_path = "./stdout.log";
std::string child_stderr_log_path = "./stderr.log";
std::ofstream stdout_log;
std::ofstream stderr_log;
stdout_log.open( child_stdout_log_path.c_str(), std::ofstream::out | std::ofstream::app );
stderr_log.open( child_stderr_log_path.c_str(), std::ofstream::out | std::ofstream::app );
// avoid cyclic dependencies between stdout and tee_out
std::ostream tmp_stdout( std::cout.rdbuf() );
std::ostream tmp_stderr( std::cerr.rdbuf() );
// writing to this ostream derivative will write to stdout log file and std::cout
teestream tee_out(tmp_stdout, stdout_log);
teestream tee_err(tmp_stderr, stderr_log);
// pop the cout/cerr buffers to the appropriate Tees' buffers
std::cout.rdbuf( tee_out.rdbuf() );
std::cerr.rdbuf( tee_err.rdbuf() );
// end - "set up the 'Tee' with the parent"
slog.log( E_INFO, "Tee Logging enabled for \"" + task_name + "\"");
// build the command to execute in the shell
std::string sourcer = ". " + environment_file + " && " + command;
// Show the user a debug print of what is going to be executed in the shell.
slog.log(E_DEBUG, "[ '" + task_name + "' ] Shell call for loading: ``" + sourcer + "``.");
exit_code_raw = system( sourcer.c_str() );
exit( WEXITSTATUS( exit_code_raw ) );
} else if ( pid > 0 )
// file descriptors for parent/child i/o
int stdout_filedes[2];
slog.log( E_DEBUG, "[ '" + task_name + "' ] STDIN/STDOUT/STDERR file descriptors created." );
// man 3 pipe
if (pipe(stdout_filedes) == -1 ) {
slog.log(E_FATAL, "[ '" + task_name + "' ] PIPE FAILED");
return SPROC_RETURN_CODES::PIPE_FAILED;
} else {
slog.log(E_DEBUG, "[ '" + task_name + "' ] file descriptors piped.");
}
// // avoids the need to take any explicit action within the child process to close file descriptors
// if (fcntl(stdout_filedes[READ_FD], F_SETFD, FD_CLOEXEC) == -1) {
// perror("fcntl");
// exit(1);
// }
// fork a process
pid_t pid = fork();
slog.log( E_DEBUG, "[ '" + task_name + "' ] Process forked. Reporting. (PID: " + std::to_string(pid) + ")" );
switch ( pid ) {
case FORK_STATES::FORK_FAILURE:
{
// fork failed
slog.log(E_FATAL, "[ '" + task_name + "' ] Fork Failed.");
break;
}
case FORK_STATES::CHILD:
{
// enter child process
slog.log(E_DEBUG, "[ '" + task_name + "' ] Entering child process.");
while ((dup2(stdout_filedes[WRITE_FD], STDOUT_FILENO) == -1) && (errno == EINTR)) {}
close( stdout_filedes[WRITE_FD] );
close( stdout_filedes[READ_FD] );
slog.log(E_DEBUG, "[ '" + task_name + "' ] DUP2 on stdout_filedes[1]->STDOUT_FILENO in child.");
// set identity context
// set gid and uid
int context_status = set_identity_context(task_name, user_name, group_name, slog);
if (!(context_status)) {
slog.log(E_FATAL, "[ '" + task_name + "' ] Identity context switch failed.");
return context_status;
}
// exit_code_raw = system( sourcer.c_str() );
int ret = execl("/bin/sh", "/bin/sh", "-c", sourcer.c_str(), (char *) NULL);
// print something useful to debug with if execl fails
slog.log(E_FATAL, "ret code: " + std::to_string(ret) + "; errno: " + strerror(errno));
// exit child -- if this is executing, you've had a failure
exit(exit_code_raw);
}
default:
{
// parent process
while ( ( pid = waitpid( pid, &exit_code_raw, 0 ) ) == -1 ) {}
close(stdout_filedes[WRITE_FD]);
// ---
// clean up Tee
stdout_log.close();
stderr_log.close();
char buffer[1000] = {0};
std::cout.flush();
// read from fd until child completes
while ( 1 ) {
ssize_t count = read(stdout_filedes[READ_FD], buffer, sizeof(buffer));
if (count == -1) {
if (errno == EINTR) {
continue;
} else {
// fork failed
slog.log( E_FATAL, "[ '" + task_name + "' ] Fork Failed");
perror("read");
exit(1);
}
} else if (count == 0) {
break;
} else {
std::cout << buffer;
std::cout.flush();
memset(&buffer[0], 0, sizeof(buffer));
// handle_child_process_output(buffer, count);
}
}
while ((pid = waitpid(pid, &exit_code_raw, 0)) == -1) {}
//waitpid( pid, &exit_code_raw, 0 );
}
}
return WEXITSTATUS( exit_code_raw );
}

View File

@ -25,13 +25,42 @@
#include <iostream>
#include <stdio.h>
#include "../Logger/Logger.h"
#include <streambuf>
#include "unistd.h"
// executes a subprocess and captures STDOUT, STDERR, and return code.
// should be able to recieve path of binary to be executed as well as any parameters
class Sproc {
public:
// call the object. returnvalue is enum representing external execution attempt not binary exit code
static int execute( std::string shell, std::string enviornment_file, std::string run_as, std::string group, std::string command, int LOG_LEVEL, std::string task_name );
static int execute(std::string shell, std::string enviornment_file, std::string user_name, std::string group_name, std::string command, int LOG_LEVEL, std::string task_name );
};
// mostly lifted from:
// http://wordaligned.org/articles/cpp-streambufs
// a teebuff implementation
class teebuf: public std::streambuf
{
public:
// Construct a streambuf which tees output to both input
// streambufs.
teebuf(std::streambuf * sb1, std::streambuf * sb2);
private:
virtual int overflow(int c);
virtual int sync();
std::streambuf * sb1;
std::streambuf * sb2;
};
// a simple helper class to create a tee stream from two input streams
class teestream : public std::ostream
{
public:
// Construct an ostream which tees output to the supplied ostreams.
teestream( std::ostream & o1, std::ostream & o2);
teebuf tbuf;
};
#endif //FTESTS_SPROC_H

View File

@ -1,6 +1,6 @@
#!/bin/bash
echo "Test var is: $TEST_VAR"
echo "TEST OUTPUT: Test var is: $TEST_VAR"
#dialog --stdout --title "Interact with me!" \
# --backtitle "This is user interaction." \

View File

@ -1,5 +1,11 @@
#!/usr/bin/bash
set -a
echo "This is output from loading the variables file."
echo "variables file says hello and set a variable named TEST_VAR"
TEST_VAR="999"