diff options
author | Jordan Bracco <href@random.sh> | 2022-03-22 21:36:58 +0000 |
---|---|---|
committer | Jordan Bracco <href@random.sh> | 2022-03-22 21:36:58 +0000 |
commit | 435531e42f3d1b1d0de291071aedb111d727e57d (patch) | |
tree | f51ae7f59750b41f6ac416e01f102801a1c420b6 /cloyster |
Initial PoC
Diffstat (limited to 'cloyster')
-rwxr-xr-x | cloyster | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/cloyster b/cloyster new file mode 100755 index 0000000..18f72ca --- /dev/null +++ b/cloyster @@ -0,0 +1,276 @@ +#!/bin/sh +XTRACE="${XTRACE:-}" +XTRACE_EXEC="${EXEC_XTRACE:-}" +DEBUG="${DEBUG:-}" +hostname="${HOSTNAME:-$(hostname)}" +_executing= +_exec_error= +_script_name=$(basename "$0" | cut -d '.' -f 1) + +# is_true VARIABLE +# return 0 if VARIABLE is "y", "yes", "t", "true", "1". Otherwise returns 1. +# Example: `if _is_true "${XTRACE}"; then set -x; fi` +is_true() { + _t=1 + case "$1" in + y) _t=0;; yes) _t=0;; + t) _t=0;; true) _t=0;; + 1) _t=0;; + *) _t=1;; + esac + return "${_t}" +} + +if is_true "${XTRACE}"; then set -x; fi + +# Exit early if we don't have `jo`. +if ! which jo > /dev/null 2>&1; then + >&2 echo '{"error":{"type":"execution_error","error":"missing_dependency","dependency":"jo"}}' + exit 1 +fi + +_debug() { + _line=$1 + shift + if is_true "${DEBUG}"; then + if is_true "${_executing}"; then + _date=$(date_now_iso_utc) + echo "${_line}" | jo "output=cloyster" "date=${_date}" "line=@-" "${@}" >> "${_cloyster_dest}" + else + >&2 echo "[${_script_name}:debug] ${_line} $@" + fi + fi +} + +_execution_error() { + _debug "fun=_execution_error() executing=${_executing}" + >&2 jo error="$(jo type=execution "$@")" +} + +. ./lib/dependencies.sh +ensure_command_exists jq +ensure_command_exists bc + +. ./lib/os.sh +_debug "os=$(os_get jq '. | @json')" +if ! is_true $(os_get "supported"); then + _execution_error error=unsupported_operating_system +fi + +. ./lib/identifiers.sh +. ./lib/datetime.sh + +## getopts +input_decode= +output_encode= +stdin= +while getopts :Bbs _opt; do + case "${_opt}" in + b) input_decode="base64";; + B) output_encode="base64";; + s) stdin=yes;; + *) _execution_error error=invalid_option option="${_opt}";; + esac +done +shift $((OPTIND - 1)) + +if is_true "${stdin}" || [ "${1}" = "-" ]; then + if [ "${1}" = "-" ]; then shift; fi + stdin=yes + _exec="STDIN" +else + _exec="${1}" + shift + if [ -z "${_exec}" ]; then + _execution_error error=no_script_specified + elif [ ! -f "${_exec}" ]; then + _execution_error error=missing_script script="${_exec}" + elif ! head -n 1 "${_exec}" | grep "/cloyster" > /dev/null 2>&1; then + _execution_error error=missing_shebang script="${_exec}" + fi +fi + +_args_json= +if [ ! "${#}" = "0" ]; then + _args_json="$(jo -d. $@)" + _debug "args_json=$_args_json" +fi + +args_get() { + _jqq="${1}" + if [ -z "${_jqq}" ]; then + _jqq="." + fi + value="$(echo ${_args_json} | jq -r "${_jqq}")" + if [ "${value}" = "null" ]; then value=; fi + echo "${value}" +} + +json_quote() { + echo "$1" | jq '. | @json' +} +json_unquote() { + echo "$1" | jq -r '.' +} + +_trap_exit() { + _debug "TRAPPED EXIT >:( $@ executing? ${_executing}" + if is_true "${_executing}"; then _exec_post "TRAPEXIT"; fi + _debug "TRAPPED EXIT >:( $@ executing? ${_executing}" + #rm "${_tmp_base}"* > /dev/null 2>&1 || true + ( _json_loggers_stop ) > /dev/null 2>&1 + _debug "$(jobs)" + _debug "bye" + if is_true "${_executing}"; then exit 1; else exit 0; fi +} +#trap _trap_exit EXIT + +_exec_name=$(basename "$_exec") +_tmp_base="/tmp/${_script_name}@${_exec_name}@$(make_random_identifier 16)" +_stdout_dest="${_tmp_base}_stdout.log" +_stderr_dest="${_tmp_base}_stderr.log" +_cloyster_dest="${_tmp_base}_cloyster.jsonstream" +_output_dest="${_tmp_base}_output.jsonstream" +_cntrl_dest="${_tmp_base}_cntrl" +_tmp_files="${_stdout_dest} ${_stderr_dest} ${_output_dest} ${_cloyster_dest} ${_cntrl_dest}" + +_debug "tmp_files: ${_tmp_files}" +for _f in ${_tmp_files}; do + if ! touch "${_f}"; then _execution_error error=tmpfile_creation_failed tmpfile="${_f}"; fi +done + +# _logger_pretty_check NAME ISO806ZDATE LINE... +_logger_pretty_echo() { + _name=$1 + _date=$2 + shift 2 + _line=$@ + _time=$(echo "$_date" | cut -d 'T' -f 2) + _bold=$(tput md) + _format_rst=$(tput me) + _color=$(tput AF 3) + _time_color=$(tput AF 59) + case "${_name}" in + stdout) _color=$(tput AF 2);; + stderr) _color=$(tput AF 1);; + cloyster) _color=$(tput AF 111);; + esac + >&2 echo -e "${_time_color}${_time} ${_color}${_bold}[${_name}]${_format_rst} ${_line}" +} + +_logger_pids="" +_json_logger() { + _name=$1 + _log=$2 + _format=$3 + _combined_log=${4:-${_output_dest}} + _json_logger_job() { + _debug "_json_logger ${_name} ${_log}" + tail -F "${_log}" | while read -r _line; do + _date=$(date_now_iso_utc) + case "${_format}" in + text) + echo "${_line}" | jo "output=${_name}" "date=${_date}" "line=@-" >> "${_combined_log}" + _logger_pretty_echo "${_name}" "${_date}" "${_line}" + ;; + rawjson) + echo "${_line}" >> "${_combined_log}" + _date=$(echo "${_line}" | jq -r '.date') + _logger_pretty_echo "${_name}" "${_date}" "${_line}" + ;; + esac + done + } + $(_json_logger_job) & + _logger_pids="${_logger_pids} $!" +} +_json_loggers_stop() { + for tailpid in $(ps auxww | grep "tail -F ${_tmp_base}" | grep -v grep | awk '{ print $2 }'); do + kill -s PIPE "${tailpid}" || true + done + for pid in ${_logger_pids}; do + kill "${pid}" > /dev/null 2>&1 || true + wait "${pid}" 2>/dev/null || true; + done +} +_json_logger "cloyster" "${_cloyster_dest}" "rawjson" +_json_logger "stdout" "${_stdout_dest}" "text" +_json_logger "stderr" "${_stderr_dest}" "text" + +_exec_post() { + trap - EXIT + _arg=$1 + _executing= + _debug "_exec_post $@" + if ! is_true "${XTRACE}" && is_true "${EXEC_XTRACE}"; then set -x; fi + end_iso=$(date_now_iso_utc) + end_us=$(posix_time_microseconds) + duration=$(bc -e "$end_us - $start_us") + _json_loggers_stop + cat ${_output_dest} | jq -sr '. | @json' > "${_output_dest}.json" + eval $(cat $_cntrl_dest) + _format_output + rm "${_tmp_base}"* || true + if [ ! "${_arg}" = "TRAPEXIT" ]; then exit "${_exec_exit_status:-1}"; fi +} +_format_output() { + jo "hostname=${hostname}" \ + "command=${_exec}" \ + "arguments=$(json_unquote ${_args_json:-'{}'})" \ + "error=${_exec_error:-false}" \ + "exit_status=${_exec_exit_status}" \ + "output=:${_output_dest}.json" \ + "result=${_exec_result}" \ + "started_at_utc=${start_iso}" \ + "finished_at_utc=${end_iso}" \ + "duration_microseconds=${duration}" +} + +_exec_pre() { + start_iso=$(date_now_iso_utc) + start_us=$(posix_time_microseconds) + if ! is_true "${XTRACE}" && is_true "${EXEC_XTRACE}"; then set -x; fi + _executing=y + trap - EXIT +} + +_exec__trap_exit() { + _exit_status=$? + _debug "Exec process exited: ${_exit_status}" + _exec__put_return "exit_status" "${_exit_status}" + if [ ! "${_exit_status}" = "0" ] && ! grep -E "^_exec_error=" ${_cntrl_dest} > /dev/null; then + _exec__put_return "error" "$(jo type=command error=exit_status)" "json" + fi +} + +_exec__put_return() { + _key=$1 + _value=$2 + _format=$3 + if [ "${_format}" = "json" ]; then + _value="$(json_quote ${_value})" + fi + echo "_exec_${_key}=${_value}" >> ${_cntrl_dest} +} + +_debug "executing: command=${_exec} args=${@}" +_exec_pre +if is_true "$stdin"; then + eval "$(cat -)" 2> "${_stderr_dest}" > "${_stdout_dest}" +else + SCRIPT="${_exec}" + # shellcheck source=/dev/null + eval "$( + trap _exec__trap_exit EXIT + _execution_error() { + _exec__put_return "error" "$( jo type=cmd $@)" "json" + exit 2 + } + return_result() { + _exec__put_return "result" "$1" "json" + exit 0 + } + . "${SCRIPT}" 2>"${_stderr_dest}" 1>"${_stdout_dest}" + )" || true +fi +_exec_post |