aboutsummaryrefslogblamecommitdiff
path: root/cloyster
blob: 16b1c238c6b085a16a239dcb2fae9072586190e4 (plain) (tree)
1
2
3
4
5
6
7
8







                                               


                                               



































                                                                                                    
                                  




                                                         

               












                                                     
                                                














                                                                       



































                                                                 






















                                                 







                                                          
 
                             

                               





                                                                          

                                                                                             

                                                      



                                                                                             









                                                                                        

                                               
















                                                                                           








                                     
               
                                          
                                                          













                                                                                                    
                    


                                   

                                                                                                    
                                       

                                  

                                            




                               





                                                     










                                                                       
                          









































                                                                                                
                  





                                                          














                                                                       
                               
   




                                                            












                                                                                                                           


                                             














































                                                                                            

          


              
#!/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