RCE Endeavors 😅

May 18, 2021

Creating a multi-language compiler system: File Watcher, Bash (5/11)

Filed under: Programming — admin @ 10:29 PM

Table of Contents:

The previous post covered the first half of the file watcher component: the background C++ process responsible for monitoring the input file directory and starting the compilation and execution process. This next part will cover the helper Bash script that actually does the work. This script is composed of two parts: the main script, which is a general script responsible creating and cleaning up the directories that will be used, and language-specific scripts responsible for running the process of compilation and execution.

Main script

The main script is what is invoked by the background process. The path and name to this script is configured via the configuration file:

"bootstrappath": "${CODE_PATH}/share/bootstrap",
"bootstrapscriptname": "bootstrap-common.sh",
...

The background process builds the command to invoke this script (see NotifyChildProcess::buildCommand in the previous post), and captures its output. The main script is shown in its entirety below:

#!/bin/bash

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
    -f|--filepath)
    FILE_PATH="$2"
    shift
    shift
    ;;
    -a|--arguments)
    ARGUMENTS_PATH="$2"
    shift
    shift
    ;;
    -i|--index)
    INDEX="$2"
    shift
    shift
    ;;
    -d|--dependenciespath)
    DEPENDENCIES_PATH="$2"
    shift
    shift
    ;;
    -w|--workspacepath)
    WORKSPACE_PATH="$2"
    shift
    shift
    ;;
    -o|--outputpath)
    OUTPUT_PATH="$2"
    shift
    shift
    ;;
    -s|--stdinPath)
    STDIN_PATH="$2"
    shift
    shift
    ;;
    -t|--timeout)
    INTERACTIVE_TIMEOUT="$2"
    shift
    shift
    ;;
    -l|--language)
    LANGUAGE="$2"
    shift
    shift
    ;;
    *)
    
    POSITIONAL+=("$1")
    shift
    ;;
esac
done
set -- "${POSITIONAL[@]}"

FILE_NAME="$(basename ${FILE_PATH})"
ARGS_FILE_EXISTS=false
STDIN_FILE_EXISTS=false

OUTPUT_NAME=${WORKSPACE_PATH}/${INDEX}/${FILE_NAME}

if [ -f "${ARGUMENTS_PATH}" ]; then
    ARGS_FILE_EXISTS=true
fi

if [ -f "${STDIN_PATH}" ]; then
    STDIN_FILE_EXISTS=true
fi

create_directories () {
    rm -rf ${WORKSPACE_PATH}/${INDEX}
    mkdir ${WORKSPACE_PATH}/${INDEX}
}

cleanup () {
    rm ${FILE_PATH}
    if [ "${ARGS_FILE_EXISTS}" = "true" ]; then
        rm ${ARGUMENTS_PATH}
    fi
    if [ "${STDIN_FILE_EXISTS}" = "true" ]; then
        rm ${STDIN_PATH}
    fi
    rm -rf ${WORKSPACE_PATH}/${INDEX}
}

copy_dependencies () {
    cp ${FILE_PATH} ${WORKSPACE_PATH}/${INDEX}
    cp -r ${DEPENDENCIES_PATH}/. ${WORKSPACE_PATH}/${INDEX}
}

move_output () {
    rm -rf ${OUTPUT_PATH}/${INDEX}
    mkdir ${OUTPUT_PATH}/${INDEX}
    mv ${OUTPUT_NAME}-output.log ${OUTPUT_PATH}/${INDEX}
}

main () {
    create_directories
    copy_dependencies
    (cd ${CODE_PATH}/share/${LANGUAGE}/bootstrap/${LANGUAGE}; source ./bootstrap.sh; run_command)
    move_output
    cleanup
    
    exit ${result}
}

main

Despite the large size of the script, half of it is just related to argument parsing and storage. The other half is for the actual functionality: creating the isolated workspace directory that the compilation and execution will take place in (create_directories function), copying the dependencies to this workspace directory (copy_dependencies function), moving the output to the output directory and cleaning up (move_output and cleanup functions). The run_command function is the language-specific function that will get invoked from its respective language directory.

Language-specific scripts

Each language has its own way to go from source code to executable. Compiled languages require a compilation step to generate an executable, while interpreted ones like Python can have the interpreter run directly on the source code. As a result, each language has its own language-specific script responsible for implementing this functionality.

For example, the run_command implementation for C files looks like this:

#!/bin/bash

run_command () {

    TIMEOUT_SECONDS_COMPILE=15s
    TIMEOUT_SECONDS_RUN=10s

    timeout ${TIMEOUT_SECONDS_COMPILE} gcc -Wall -std=c17 -Wno-deprecated ${OUTPUT_NAME} -o ${OUTPUT_NAME}.out >> ${OUTPUT_NAME}-output.log 2>&1
    result=$?
    
    if [ $result -eq 0 ]
    then
        chmod 753 ${OUTPUT_NAME}-output.log
        if [ "${ARGS_FILE_EXISTS}" = "true" ]; then
            ARGUMENTS=$(cat ${ARGUMENTS_PATH})
        fi
        if [ "${STDIN_FILE_EXISTS}" = "true" ]; then
            TIMEOUT_SECONDS_RUN=${INTERACTIVE_TIMEOUT}
            STDIN_ARGUMENTS="-s ${STDIN_PATH}"
        fi
        
        sudo su-exec exec ${EXEC_PATH}/executor -t ${TIMEOUT_SECONDS_RUN} ${STDIN_ARGUMENTS} -o ${OUTPUT_NAME}-output.log -f ${OUTPUT_NAME}.out ${ARGUMENTS}
        result=$?
    fi
}

This function begins by invoking the GCC compiler to create an executable. The output of this process is captured via stream redirection into an output file, so if the compilation fails then the cause will be still be captured and presented to the user. Once the executable is generated, the execution component is called to run it. Both of these steps come with a timeout value in order to prevent issues with hanging compiler processes or infinitely running executables.

Once the execution component has completed, the process return code is returned to the main script and subsequently the background process. The implementation of the execution component will be covered in-depth in the next post. After the discussion of the execution component, the end-to-end details of how the system works will be complete.

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

 

Powered by WordPress