blob: 16b1c238c6b085a16a239dcb2fae9072586190e4 (
plain) (
tree)
|
|
#!/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
|