#!/bin/sh XTRACE="${XTRACE:-}" XTRACE_EXEC="${EXEC_XTRACE:-}" DEBUG="${DEBUG:-}" hostname="${HOSTNAME:-$(hostname)}" _executing= _exec_error= _script_name=$(basename "$0" | cut -d '.' -f 1) CLOYSTER_SCRIPT_ROOT=$(dirname "$0" | realpath) _testing= _silent= # 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 } # Shadowed in the command subshell _execution_error() { _debug "fun=_execution_error() executing=${_executing}" >&2 jo error="$(jo type=execution "$@")" } . ./lib/vars.sh . ./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 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 ## getopts input_decode= output_encode= stdin= while getopts :BbsTqVh _opt; do case "${_opt}" in b) input_decode="base64";; B) output_encode="base64";; s) stdin=yes;; T) _testing=yes;; q) _silent=yes;; V) echo "cloyster vXXX??" exit 0 ;; h) if [ -n "{_exec}" ] && [ ! "${_exec}" = "stdin" ]; then echo "${_exec}: cloyster command" echo "${_exec} [exec_options] [command json arguments]" else echo "cloyster help:" fi echo "execution options:" echo -e " -s, -\texecute command from stdin" echo -e " -q\tquiet (do not copy stdout/stderr to stderr)" echo -e " -b\tdecode input from base64" echo -e " -B\tencode output to base64" echo -e " -T\trun tests" echo -e " -V\tcloyster version" exit 0 ;; *) _execution_error error=invalid_option option="${_opt}";; esac done shift $((OPTIND - 1)) _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 '.' } _cloyster_trap_exit() { if is_true "${_executing}"; then _debug "_cloyster_trap_exit: ignoring: is executing" else _debug "_cloyster_trap_exit: bye" ( _json_loggers_stop ) > /dev/null 2>&1 || true ( _cloyster_delete_tmp_dir ) > /dev/null 2>&1 || true fi } trap _cloyster_trap_exit EXIT _exec_name=$(basename "$_exec") _tmp_dir="/tmp/${_script_name}@${_exec_name}@$(make_random_identifier 16)" _stdout_dest="${_tmp_dir}/stdout.log" _stderr_dest="${_tmp_dir}/stderr.log" _cloyster_dest="${_tmp_dir}/cloyster.jsonstream" _output_dest="${_tmp_dir}/output.jsonstream" _cntrl_dest="${_tmp_dir}/return" _tmp_files="${_stdout_dest} ${_stderr_dest} ${_output_dest} ${_cloyster_dest} ${_cntrl_dest}" _debug "tmp_dir: ${_tmp_dir} tmp_files: ${_tmp_files}" mkdir "${_tmp_dir}" for _f in ${_tmp_files}; do if ! touch "${_f}"; then _execution_error error=tmpfile_creation_failed tmpfile="${_f}"; fi done _cloyster_delete_tmp_dir() { if [ -n "${_tmp_dir}" ] && ! is_true "$_tmp_dir_deleted" && [ -d "${_tmp_dir}" ]; then _debug "Cleaning ${_tmp_dir}" rm -rfx "${_tmp_dir}" _tmp_dir_deleted=yes else _debug "Tmpdir already cleaned!" fi } # _logger_pretty_check NAME ISO806ZDATE LINE... _logger_pretty_echo() { if ! is_true "${_silent}"; then _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}" fi } _logger_pids="" _json_logger() { _name=$1 _log=$2 _format=$3 _combined_log=${4:-${_output_dest}} _json_logger_job() { trap - EXIT _debug "_json_logger ${_name} ${_log}" stdbuf -oL 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() { if ! is_true "$_json_loggers_stop_done"; then for tailpid in $(ps auxww | grep "tail -F ${_tmp_dir}" | 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 _logger_pids= _json_loggers_stop_done=yes fi #sleep 4 } _json_logger "cloyster" "${_cloyster_dest}" "rawjson" _json_logger "stdout" "${_stdout_dest}" "text" _json_logger "stderr" "${_stderr_dest}" "text" _exec_post() { _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 _cloyster_delete_tmp_dir 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} } _cloyster_exec() { _debug "executing: command=${_exec} args=${@}" _exec_pre if is_true "$stdin"; then eval "$(cat -)" 2> "${_stderr_dest}" > "${_stdout_dest}" else SCRIPT="${_exec}" if [ "${_exec}" = "stdin" ]; then CLOYSTER_COMMAND=stdin CLOYSTER_COMMAND_FILE=/dev/null CLOYSTER_COMMAND_DIR=/dev/null _debug "Running: stdin" else CLOYSTER_COMMAND="$(basename "${_exec}")" CLOYSTER_COMMAND_DIR="$(realpath "$(dirname "${_exec}")")" CLOYSTER_COMMAND_FILE="${CLOYSTER_COMMAND_DIR}/${CLOYSTER_COMMAND}" _debug "Running: ${CLOYSTER_COMMAND} (${CLOYSTER_COMMAND_FILE})" fi CLOYSTER_TMP_DIR="${_tmp_dir}" _cloyster_test_tests= # shellcheck source=/dev/null ( trap _exec__trap_exit EXIT _execution_error() { _exec__put_return "error" "$( jo type=cmd $@)" "json" exit 2 } include_cmd_dir() { if [ "${CLOYSTER_COMMAND}" = "stdin" ]; then _execution_error function=include_cmd_dir error=include_cmd_dir_with_stdin msg="cannot be used with stdin commands" exit 1 else if [ -z "${1}" ]; then _execution_error function=include_cmd_dir error=missing_argument argument=file; exit 1; fi _debug "Including ${CLOYSTER_COMMAND_DIR}/${1}" . "${CLOYSTER_COMMAND_DIR}/${1}" 2>"${_stderr_dest}" 1>"${_stdout_dest}" fi } return_result() { _exec__put_return "result" "$1" "json" exit 0 } return_error() { _exec__put_return "error" "$(jo type=cmd "$@")" "json" exit 1 } alias RUN="{ runcmd() {" alias ENDR="}; }" . "${CLOYSTER_SCRIPT_ROOT}/lib/test.sh" cloyster_test_init # if is_true "${_testing}"; then _test() { id=$(make_random_identifier 12) export _cloyster_test_tests="${_cloyster_test_tests} ${id}" setvar "_cloyster_test_tests_${id}__name" "$1" shift setvar "_cloyster_test_tests_${id}__args" "$*" alias TEST="{ setvar \"_cloyster_test_tests_${id}__line\" \$LINENO; _test_${id}() {" alias ENDT="}; unalias TEST; unalias ENDT; }" } # else # _test() { # alias TEST="{ __cloyster_test_disabled() {" # alias ENDT="}; unalias TEST; unalias ENDT; }" # } # fi cd "${CLOYSTER_TMP_DIR}" _debug "Including command ${CLOYSTER_COMMAND_FILE}" . "${CLOYSTER_COMMAND_FILE}" 2>"${_stderr_dest}" 1>"${_stdout_dest}" if [ "$_executing" = "y" ]; then # if command -v "runcmd"; then if is_true "${_testing}"; then cloyster_test_run else _debug "runcmd!" runcmd "$@" 2>"${_stderr_dest}" 1>"${_stdout_dest}" fi # else # _execution_error error=no_run_function # fi fi ) || true fi _exec_post } _cloyster_exec