diff --git a/bin/compose-core b/bin/compose-core index 5652290..affbd97 100755 --- a/bin/compose-core +++ b/bin/compose-core @@ -2737,31 +2737,72 @@ export -f service:state charm:upstream-version() { local charm="$1" version cache_file="$state_tmpdir/$FUNCNAME.cache.$1" path if [ -e "$cache_file" ]; then - cat "$cache_file" - return 0 - fi - if ! path=$(charm.has_direct_action "$charm" "upstream-versions"); then - return 0 + { + read-0 errlvl + cat + } <"$cache_file" + return $errlvl fi - version=$("$path" -l 1) || { - err "Failed to get upstream version for ${DARKYELLOW}$charm${NORMAL}." - return 1 - } - if path=$(charm.has_direct_action "$charm" "upstream-version-normalize"); then - version=$("$path" "$version") || { - err "Failed to normalize upstream version for ${DARKYELLOW}$charm${NORMAL}." + ( + if ! mkdir "$cache_file.lock" 2>/dev/null; then + while true; do + sleep 0.1 + [ -d "${cache_file}.lock" ] || break + done + if [ -e "$cache_file" ]; then + { + read-0 errlvl + if [ "$errlvl" == 0 ]; then + cat + else + cat >&2 + fi + } <"$cache_file" + return $errlvl + fi return 1 - } - fi - echo "$version" > "$cache_file" - e "$version" + fi + trap_add EXIT,ERR "rmdir \"${cache_file}\".lock" + if ! path=$(charm.has_direct_action "$charm" "upstream-versions"); then + touch "$cache_file" + return 0 + fi + rm -f "${cache_file}.wip" + touch "${cache_file}.wip" + ( + version=$("$path" -l 1) + errlvl=$? + if [ "$errlvl" != 0 ]; then + err "Action ${WHITE}upstream-versions${NORMAL} failed for ${DARKPINK}$charm${NORMAL}." + return $errlvl + fi + if path=$(charm.has_direct_action "$charm" "upstream-version-normalize"); then + version=$("$path" "$version") + errlvl=$? + if [ "$errlvl" != 0 ]; then + err "Failed to normalize upstream version for ${DARKPINK}$charm${NORMAL}." + return $errlvl + fi + fi + echo "$version" + ) > "${cache_file}.wip" 2>&1 + errlvl=$? + p0 "$errlvl" > "${cache_file}" + if [ "$errlvl" != 0 ]; then + cat "${cache_file}.wip" | tee -a "${cache_file}" >&2 + rm "${cache_file}.wip" + return $errlvl + fi + cat "${cache_file}.wip" | tee -a "${cache_file}" + rm "${cache_file}.wip" + ) } service:upstream-version() { local service="$1" version - charm=$(get_service_charm "$service") || return 1 - version=$(charm:upstream-version "$charm") || return 1 + charm=$(get_service_charm "$service") || return $? + version=$(charm:upstream-version "$charm") || return $? e "$version" } export -f service:upstream-version @@ -5703,14 +5744,81 @@ if [ "${PIPESTATUS[0]}" != 0 ]; then fi [ "$action" == "build" ] && exit 0 +state:fields:resolve-parallel() { + local cols=("$@") service jobs state_msg out errlvl col + first_job=1 + tick_pid= + concurrent_jobs=0 + MAX_CONCURRENT_JOBS=$(nproc) + for col in "${cols[@]}"; do + for service in "${services_args[@]}"; do + if [ "$concurrent_jobs" -ge "$MAX_CONCURRENT_JOBS" ]; then + wait -n # -p job_id ## not supported in this version of bash + ## job list is not accurate, but the number of elt is + ((concurrent_jobs--)) + fi + ( + out=$( + case "${col//_/-}" in + root) + if [[ " ${compose_yml_services[*]} " == *" ${service} "* ]]; then + echo "1" + else + echo "0" + fi + ;; + name) e "$service" ;; + charm) get_service_charm "$service" ;; + state) service:state "$service" ;; + type) get_service_type "$service" ;; + upstream-version) service:upstream-version "$service" ;; + *) + if has_service_action "$service" "get-$col" >/dev/null; then + state_msg=$(run_service_action "$service" "get-$col") || exit 1 + if [[ "$state_msg" == *$'\n'* ]]; then + e "${state_msg%%$'\n'*}" + else + e "${state_msg}" + fi + fi + ;; + esac 2>&1 + ) + errlvl="$?" + p0 "$service" "$col" "$errlvl" "$out" + ) & + jobs=("${jobs[@]}" $!) + ((concurrent_jobs++)) + if [ -n "$first_job" ]; then + ## launch tick + ( + while true; do + sleep 0.1 + p0 "" "" "" "" + done + ) & + tick_pid=$! + first_job= + fi + done + done + wait "${jobs[@]}" + if [ -n "$tick_pid" ]; then + kill "$tick_pid" + fi +} + if [ "$action" == "status" ]; then + if ! [ -t 1 ]; then + state_raw_output=1 + fi + if [[ -n "${state_all_services}" ]] || [[ "${#state_filters[@]}" -gt 0 ]]; then compose_yml_services=($(compose:yml:root:services)) || exit 1 fi if [[ -n "${state_all_services}" ]]; then state_columns=("root" ${state_columns[@]}) fi - state_columns_raw=() for col in "${state_columns[@]}"; do if [[ "$col" =~ ^[+-] ]]; then @@ -5731,13 +5839,136 @@ if [ "$action" == "status" ]; then esac fi done + declare -A state_columns_idx=() + declare -A filter_idx=() + filter_cols=() + non_filter_cols=("${state_columns_raw[@]}") + for filter in "${state_filters[@]}"; do + IFS="=" read -r key value <<<"$filter" + if [[ " ${non_filter_cols[*]} " == *" $key "* ]]; then + ## remove from non_filter_cols + non_filter_cols=(${non_filter_cols[*]/$key}) + fi + state_columns_idx["$col"]="${#filter_cols[@]}" + filter_cols+=("${key}") + done + tot_nb_cols=$(( ${#non_filter_cols[@]} + ${#filter_cols[@]} )) - while read-0-err E "${state_columns_raw[@]}"; do - values=() - for col in "${state_columns_raw[@]}"; do - color= - value="${!col}" - if [ -z "$state_raw_output" ]; then + ## make services_idx + declare -A services_idx=() + idx=0 + for service in "${services_args[@]}"; do + services_idx["$service"]=$((idx++)) + done + ## make state_columns_idx + idx=0 + for col in "${non_filter_cols[@]}"; do + state_columns_idx["$col"]=$((${#filter_cols[@]} + idx++)) + done + + values=() ## all values + new_service_args=("${services_args[@]}") ## will remove service not satisfying filters + while read-0 service col E out; do + if [[ " ${new_service_args[*]} " != *" $service "* ]]; then + continue + fi + col_index="${state_columns_idx[$col]}" + service_index="${services_idx[$service]}" + values[$((service_index * tot_nb_cols + col_index))]="$out" + ## check if all filter are valuated and satisfied + for filter in "${state_filters[@]}"; do + IFS="=" read -r key value <<<"$filter" + col_index="${state_columns_idx[$key]}" + if [ -z "${values[$((service_index * tot_nb_cols + col_index))]}" ]; then + break + fi + if [ "${values[$((service_index * tot_nb_cols + col_index))]}" != "$value" ]; then + new_service_args=(${new_service_args[*]/"$service"}) + break + fi + done + done < <(state:fields:resolve-parallel "${filter_cols[@]}") + + + services_args=("${new_service_args[@]}") + if [ "${#services_args[@]}" == 0 ]; then + warn "No services found matching the filters." >&2 + exit 0 + fi + + spinner_chars="⣷⣯⣟⡿⢿⣻⣽⣾" + spinner_idx=0 + spinner_bg_steps=4 + spinner_bg_dir=1 + first_draw=1 + last_draw= + if [ -z "$state_raw_output" ]; then + echo -en "\e[?25l"; stty -echo 2>/dev/null + trap_add EXIT,ERR "echo -en '\e[?25h'; stty echo 2>/dev/null" + fi + errors=() + declare -A errors_hash_idx=() + error_idx=0 + values_valued=0 + values_total=$(( ${#services_args[@]} * ${#state_columns_raw[@]} )) + while read-0 service col E out; do + if [ -n "$service" ]; then + col_index="${state_columns_idx[$col]}" + service_index="${services_idx[$service]}" + if [[ "$E" != 0 ]]; then + error_hash=$(H "$col" "$E" "$out") + matching_error_idx="${errors_hash_idx[$error_hash]}" + if [[ -z "${matching_error_idx}" ]]; then + errors+=("$error_idx:$service:$col:$E:$out") + out="!Err[$((error_idx))]" + errors_hash_idx["$error_hash"]="$error_idx" + error_idx=$((error_idx + 1)) + else + ## find the error to add the service + error="${errors[$matching_error_idx]}" + error="${error#*:}" + error_service="${error%%:*}" + error_tail="${error#*:}" + errors[$matching_error_idx]="$matching_error_idx:$error_service,$service:$error_tail" + out="!Err[$((matching_error_idx))]" + fi + fi + values[$((service_index * tot_nb_cols + col_index))]="$out" + values_valued=$((values_valued + 1)) + if [[ "$values_valued" != "$values_total" ]]; then + last_draw=1 + continue + fi + fi + [ -n "$state_raw_output" ] && continue + + ## Draw table + if [ -n "$first_draw" ]; then + first_draw= + else + ## move up one line per service + printf "\033[%dA" "${#services_args[@]}" + fi + if [[ "$spinner_bg_dir" == "1" ]]; then + spinner_bg_step=$((spinner_bg_step + 1)) + if [ "$spinner_bg_step" -ge "$spinner_bg_steps" ]; then + spinner_idx=$(( (spinner_idx + 1) % ${#spinner_chars} )) + spinner_bg_dir=0 + fi + else + spinner_bg_step=$((spinner_bg_step - 1)) + if [ "$spinner_bg_step" -le 0 ]; then + spinner_idx=$(( (spinner_idx + 1) % ${#spinner_chars} )) + spinner_bg_dir=1 + fi + fi + SPINNERGRAY=$'\e[38;5;28;48;5;'"$((232 + spinner_bg_step))"'m' + + while read-0-err E "${state_columns_raw[@]}"; do + line_values=() + for col in "${state_columns_raw[@]}"; do + color= + value="${!col}" read -r -- value_trim <<<"${!col}" case "${col//_/-}" in root) @@ -5767,107 +5998,115 @@ if [ "$action" == "status" ]; then if [[ "${value_trim}" == "N/A" ]]; then color=gray fi + if [[ "$value_trim" == "!Err"* ]]; then + color=darkred + fi + if [[ "$spinner_chars" == *"$value_trim"* ]]; then + color=spinnergray + fi ;; esac color="${color^^}" - fi - if [ -n "$color" ]; then - values+=("${!color}$value${NORMAL}") - else - values+=("$value") - fi - done - first=1 - for value in "${values[@]}"; do - if [ -n "$first" ]; then - first= - else - if [ -n "$state_raw_output_nul" ]; then - printf "\0" + if [ -n "$color" ]; then + line_values+=("${!color}$value${NORMAL}") + else + line_values+=("$value") + fi + done + first=1 + for value in "${line_values[@]}"; do + if [ -n "$first" ]; then + first= else printf " " fi - fi - printf "%s" "$value" - done - if [ -n "$state_raw_output_nul" ]; then - printf "\0" - else + printf "%s" "$value" + done printf "\n" + done < <( + set -o pipefail + for service in "${services_args[@]}"; do + for col in "${state_columns_raw[@]}"; do + col_index="${state_columns_idx[$col]}" + service_index="${services_idx[$service]}" + value_idx="$((service_index * tot_nb_cols + col_index))" + if ! [[ -v "values[$value_idx]" ]]; then + p0 " ${spinner_chars:$spinner_idx:1} " + elif [ -z "${values[$((service_index * tot_nb_cols + col_index))]}" ]; then + p0 "N/A" + else + p0 "${values[$((service_index * tot_nb_cols + col_index))]}" + fi + done + done | { + if [ -z "$state_raw_output" ]; then + col-0:normalize:size "${state_columns_align}" + else + cat + fi + } + echo 0 + ) + if [ "$E" != 0 ]; then + err "Unexpected failure" + exit $E fi - done < <( - set -o pipefail - filter_cols=() - for filter in "${state_filters[@]}"; do - IFS="=" read -r key value <<<"$filter" - ## if not already in state_columns_raw - [[ " ${state_columns_raw[*]} " == *" $key "* ]] || - filter_cols+=("${key//-/_}") + done < <(state:fields:resolve-parallel "${non_filter_cols[@]}") + + for error in "${errors[@]}"; do + echo "" >&2 + idx=${error%%:*}; error=${error#*:} + service=${error%%:*}; error=${error#*:} + col=${error%%:*}; error=${error#*:} + E=${error%%:*}; error=${error#*:} + service_list_str="" + services=(${service//,/ }) + first=1 + for service in "${services[@]}"; do + if [ -n "$first" ]; then + first= + else + service_list_str+=", " + fi + service_list_str+="${DARKYELLOW}$service${NORMAL}" done + echo "${RED}Error${DARKRED}[$idx]:${NORMAL} while computing" \ + "${WHITE}$col${NORMAL} for $service_list_str" >&2 + echo "$error" | prefix " ${GRAY}|${NORMAL} " >&2 + echo " ${GRAY}..${NORMAL} ${WHITE}Exited${NORMAL} with errorlevel ${DARKRED}$E${NORMAL}" >&2 + done + if [[ "${#errors[@]}" -gt 0 ]]; then + exit 1 + fi + + if [ -n "$state_raw_output" ]; then + for service in "${services_args[@]}"; do - declare -A values=() - for col in "${state_columns_raw[@]}" "${filter_cols[@]}"; do - case "${col//_/-}" in - root) - if [[ " ${compose_yml_services[*]} " == *" ${service} "* ]]; then - value="1" - else - value="0" - fi - ;; - name) value="$service" ;; - charm) - value=$(get_service_charm "$service") || { echo 1; exit 1; } - ;; - state) - value=$(service:state "$service") || { echo 1; exit 1; } - ;; - type) - value=$(get_service_type "$service") || { echo 1; exit 1; } - ;; - upstream-version) - value=$(service:upstream-version "$service") || { echo 1; exit 1; } - value=${value:-N/A} - ;; - *) - if has_service_action "$service" "get-$col" >/dev/null; then - state_msg=$(run_service_action "$service" "get-$col") || { echo 1; exit 1 ; } - if [[ "$state_msg" == *$'\n'* ]]; then - value="${state_msg%%$'\n'*}" - ## XXXvlab: For now, these are not used, but we could - ## display them in additional lines (in same "cell") - msgs="${state_msg#*$'\n'}" - else - value=${state_msg} - fi - else - value="N/A" - fi - ;; - esac - values["$col"]="$value" - done - for filter in "${state_filters[@]}"; do - IFS="=" read -r key value <<<"$filter" - [[ "${values[$key]}" != "$value" ]] && - continue 2 - done + first=1 for col in "${state_columns_raw[@]}"; do - p0 "${values[$col]}" + col_index="${state_columns_idx[$col]}" + service_index="${services_idx[$service]}" + value_idx="$((service_index * tot_nb_cols + col_index))" + value="${values[$value_idx]}" + if [ -n "$first" ]; then + first= + else + if [ -n "$state_raw_output_nul" ]; then + printf "\0" + else + printf " " + fi + fi + printf "%s" "$value" done - done | { - if [ -z "$state_raw_output" ]; then - col-0:normalize:size "${state_columns_align}" + if [ -n "$state_raw_output_nul" ]; then + printf "\0" else - cat + printf "\n" fi - } - echo 0 - ) - if [ "$E" != 0 ]; then - echo "E: '$E'" >&2 - exit 1 + done fi + exit 0 fi