@beplus/be 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/be +175 -32
  2. package/package.json +1 -1
package/bin/be CHANGED
@@ -25,6 +25,16 @@ verbose_log() {
25
25
  fi
26
26
  }
27
27
 
28
+ #
29
+ # warn_log <type> <msg>
30
+ # Like verbose_log, but shown even with --quiet: for trouble be works around, such as a download
31
+ # it is about to try again.
32
+ #
33
+
34
+ warn_log() {
35
+ >&2 printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}\n" "$1" "$2"
36
+ }
37
+
28
38
  #
29
39
  # Exit with the given <msg ...>
30
40
  #
@@ -64,7 +74,7 @@ function n_grep() {
64
74
  # Setup and state
65
75
  #
66
76
 
67
- VERSION="v0.11.0"
77
+ VERSION="v0.12.0"
68
78
 
69
79
  BE_PREFIX="${BE_PREFIX-/usr/local}"
70
80
  BE_PREFIX=${BE_PREFIX%/}
@@ -103,7 +113,7 @@ if [[ -t 1 ]]; then
103
113
  else
104
114
  CURL_OPTIONS+=( "--silent" )
105
115
  fi
106
- WGET_OPTIONS=( "-q" "-O-" )
116
+ WGET_OPTIONS=( "-q" )
107
117
 
108
118
  # Set by set_active_beplus
109
119
  g_active_beplus=
@@ -142,6 +152,21 @@ else
142
152
  readonly SGR_CYAN=
143
153
  fi
144
154
 
155
+ # A download that fails in a way the next attempt may not is tried again (see fetch), up to
156
+ # BE_DOWNLOAD_ATTEMPTS attempts in all: GitHub, which hosts the beplus CLI releases, answers 504
157
+ # now and then, most often to Actions runners. The first retry waits BE_DOWNLOAD_RETRY_DELAY
158
+ # seconds, and each later one twice as long as the one before, so the defaults wait 5, 10, 20 and
159
+ # 40 seconds. BE_DOWNLOAD_ATTEMPTS=1 turns retrying off.
160
+
161
+ BE_DOWNLOAD_ATTEMPTS="${BE_DOWNLOAD_ATTEMPTS:-5}"
162
+ BE_DOWNLOAD_RETRY_DELAY="${BE_DOWNLOAD_RETRY_DELAY:-5}"
163
+ # No leading zero: arithmetic would read 08 as a bad octal number.
164
+ [[ "${BE_DOWNLOAD_ATTEMPTS}" =~ ^[1-9][0-9]*$ ]] \
165
+ || abort "BE_DOWNLOAD_ATTEMPTS is '${BE_DOWNLOAD_ATTEMPTS}', not a whole number of 1 or more (1 turns retrying off)"
166
+ [[ "${BE_DOWNLOAD_RETRY_DELAY}" =~ ^(0|[1-9][0-9]*)$ ]] \
167
+ || abort "BE_DOWNLOAD_RETRY_DELAY is '${BE_DOWNLOAD_RETRY_DELAY}', not a whole number of seconds"
168
+ readonly BE_DOWNLOAD_ATTEMPTS BE_DOWNLOAD_RETRY_DELAY
169
+
145
170
  #
146
171
  # set_arch <arch> to override $(uname -a)
147
172
  #
@@ -629,7 +654,7 @@ install() {
629
654
  # sha256sum writes it; v1.0.5 lists `<name>: <hash>`.
630
655
  local sums_url="${url%/*}/SHA256SUMS"
631
656
  local sums
632
- sums="$(do_get_index "${sums_url}")" || abort "failed to download SHA256SUMS for $version (${sums_url})"
657
+ sums="$(fetch text "${sums_url}")" || abort "failed to download SHA256SUMS for $version (${sums_url})"
633
658
  local checksum="$(echo "${sums}" | awk -v name="${url##*/}" '$2 == name { print $1; exit } $1 == name ":" { print $2; exit }')"
634
659
  [[ -n "${checksum}" ]] || abort "no checksum for ${url##*/} in ${sums_url}"
635
660
 
@@ -641,7 +666,7 @@ install() {
641
666
 
642
667
  log fetch "$url"
643
668
  local archive="${dir}/${url##*/}"
644
- if ! do_get "${url}" > "${archive}"; then
669
+ if ! fetch file "${url}" "${archive}"; then
645
670
  rm -f "${archive}"
646
671
  abort "failed to download archive for $version"
647
672
  fi
@@ -675,19 +700,158 @@ set_quiet() {
675
700
 
676
701
  #
677
702
  # Synopsis: do_get [option...] url
678
- # Call curl or wget with combination of global and passed options.
703
+ # Call curl or wget with combination of global and passed options, once: doctor shows the command.
704
+ # Downloads go through fetch, which tries again.
679
705
  #
680
706
 
681
707
  function do_get() {
682
708
  if command -v curl &> /dev/null; then
683
709
  curl "${CURL_OPTIONS[@]}" "$@"
684
710
  elif command -v wget &> /dev/null; then
685
- wget "${WGET_OPTIONS[@]}" "$@"
711
+ wget "${WGET_OPTIONS[@]}" -O- "$@"
712
+ else
713
+ abort "curl or wget command required"
714
+ fi
715
+ }
716
+
717
+ #
718
+ # Synopsis: fetch head|text|file url [file]
719
+ # Fetch <url> with curl, or with wget where there is no curl:
720
+ # head ask for the headers only, to check that it is there
721
+ # text output the body, such as a release index or SHA256SUMS
722
+ # file save the body as <file>, with a progress bar where be shows one
723
+ # An attempt that fails in a way the next one may not (is_transient_failure) is tried again, up
724
+ # to BE_DOWNLOAD_ATTEMPTS attempts in all, the waits doubling from BE_DOWNLOAD_RETRY_DELAY
725
+ # seconds. Returns the status of the last attempt.
726
+ #
727
+
728
+ function fetch() {
729
+ local mode="$1"
730
+ local url="$2"
731
+ local body="/dev/null"
732
+ if [[ "${mode}" == "file" ]]; then
733
+ body="$3"
734
+ elif [[ "${mode}" == "text" ]]; then
735
+ # Output only once an attempt succeeds, so that a failed one leaves no part of a body behind.
736
+ body="$(mktemp)" || return 1
737
+ fi
738
+
739
+ local attempt=1
740
+ local delay="${BE_DOWNLOAD_RETRY_DELAY}"
741
+ local code
742
+ local status
743
+ while true; do
744
+ # Start the body afresh: curl leaves the file as it was when an attempt receives nothing.
745
+ [[ "${body}" != "/dev/null" ]] && : > "${body}"
746
+ code="$(fetch_attempt "${mode}" "${url}" "${body}")"
747
+ status=$?
748
+ [[ "${status}" -eq 0 ]] && break
749
+ is_transient_failure "${status}" "${code}" || break
750
+ if [[ "${attempt}" -ge "${BE_DOWNLOAD_ATTEMPTS}" ]]; then
751
+ if [[ "${attempt}" -gt 1 ]]; then
752
+ warn_log "gave up" "${url} $(display_fetch_failure "${status}" "${code}") on attempt ${attempt} of ${BE_DOWNLOAD_ATTEMPTS}"
753
+ fi
754
+ break
755
+ fi
756
+ local unit="seconds"
757
+ [[ "${delay}" -eq 1 ]] && unit="second"
758
+ warn_log "retrying" "${url} $(display_fetch_failure "${status}" "${code}"); attempt $(( attempt + 1 )) of ${BE_DOWNLOAD_ATTEMPTS} in ${delay} ${unit}"
759
+ sleep "${delay}"
760
+ attempt=$(( attempt + 1 ))
761
+ delay=$(( delay * 2 ))
762
+ done
763
+
764
+ if [[ "${mode}" == "text" ]]; then
765
+ [[ "${status}" -eq 0 ]] && cat "${body}"
766
+ rm -f "${body}"
767
+ fi
768
+ return "${status}"
769
+ }
770
+
771
+ #
772
+ # Synopsis: fetch_attempt head|text|file url body
773
+ # One request for <url>, as fetch describes, with the body written into the file <body>. Outputs
774
+ # the HTTP status of the last response (000 when none came) and returns curl's or wget's status.
775
+ #
776
+
777
+ function fetch_attempt() {
778
+ local mode="$1"
779
+ local url="$2"
780
+ local body="$3"
781
+ if command -v curl &> /dev/null; then
782
+ local options=( "${CURL_OPTIONS[@]}" )
783
+ case "${mode}" in
784
+ head) options+=( "--silent" "--head" ) ;;
785
+ # --silent to suppress progress et al
786
+ text) options=( "--silent" "--compressed" "${options[@]}" ) ;;
787
+ esac
788
+ curl "${options[@]}" --output "${body}" --write-out '%{http_code}' "${url}"
789
+ elif command -v wget &> /dev/null; then
790
+ # One try: fetch does the trying again (GNU wget would retry a 504 twenty times itself, but
791
+ # not a 503). --server-response prints each response's status line on stderr.
792
+ local options=( "${WGET_OPTIONS[@]}" "--tries=1" "--server-response" )
793
+ if [[ "${mode}" == "head" ]]; then
794
+ options+=( "--spider" )
795
+ else
796
+ options+=( "-O" "${body}" )
797
+ fi
798
+ local response
799
+ response="$(wget "${options[@]}" "${url}" 2>&1)"
800
+ local status=$?
801
+ echo "${response}" | awk '$1 ~ /^HTTP\// { code = $2 } END { print (code == "" ? "000" : code) }'
802
+ return "${status}"
686
803
  else
687
804
  abort "curl or wget command required"
688
805
  fi
689
806
  }
690
807
 
808
+ #
809
+ # Synopsis: is_transient_failure status http-code
810
+ # Whether an attempt that failed with curl's or wget's <status>, after the server answered
811
+ # <http-code> (000 for no answer), may succeed when made again: the server answered 408, 429, 500,
812
+ # 502, 503 or 504, or it gave no full answer, because its name did not resolve, it could not be
813
+ # reached, the connection dropped or it timed out. Any other answer, such as a 404, is final, and
814
+ # so is a failure of be's own, such as a file it cannot write.
815
+ #
816
+
817
+ function is_transient_failure() {
818
+ case "$2" in
819
+ 408|429|500|502|503|504) return 0 ;;
820
+ [45][0-9][0-9]) return 1 ;;
821
+ esac
822
+ if command -v curl &> /dev/null; then
823
+ # https://curl.se/libcurl/c/libcurl-errors.html: the proxy or host did not resolve (5, 6), no
824
+ # connection (7), an HTTP/2 or HTTP/3 error (16, 92, 95), a partial body (18), a timeout
825
+ # (28), a failed TLS handshake (35), an empty reply (52), sending or receiving failed (55, 56).
826
+ case "$1" in
827
+ 5|6|7|16|18|28|35|52|55|56|92|95) return 0 ;;
828
+ esac
829
+ else
830
+ # GNU wget's "network failure". BusyBox wget exits 1 for any failure, so there only what the
831
+ # server answered is tried again.
832
+ [[ "$1" -eq 4 ]] && return 0
833
+ fi
834
+ return 1
835
+ }
836
+
837
+ #
838
+ # Synopsis: display_fetch_failure status http-code
839
+ # How a fetch attempt failed, for a log message.
840
+ #
841
+
842
+ function display_fetch_failure() {
843
+ case "$2" in
844
+ [45][0-9][0-9]) echo "answered HTTP $2" ;;
845
+ *)
846
+ if command -v curl &> /dev/null; then
847
+ echo "failed (curl exit $1)"
848
+ else
849
+ echo "failed (wget exit $1)"
850
+ fi
851
+ ;;
852
+ esac
853
+ }
854
+
691
855
  #
692
856
  # Synopsis: display_sha256 file
693
857
  # sha256sum where it exists, otherwise shasum, which macOS has. Both hash stdin rather than a
@@ -705,23 +869,6 @@ function display_sha256() {
705
869
  echo "${line%% *}"
706
870
  }
707
871
 
708
- #
709
- # Synopsis: do_get_index [option...] url
710
- # Call curl or wget with combination of global and passed options,
711
- # with options tweaked to be more suitable for getting index.
712
- #
713
-
714
- function do_get_index() {
715
- if command -v curl &> /dev/null; then
716
- # --silent to suppress progress et al
717
- curl --silent --compressed "${CURL_OPTIONS[@]}" "$@"
718
- elif command -v wget &> /dev/null; then
719
- wget "${WGET_OPTIONS[@]}" "$@"
720
- else
721
- abort "curl or wget command required"
722
- fi
723
- }
724
-
725
872
  #
726
873
  # Synopsis: remove_versions version ...
727
874
  #
@@ -819,13 +966,8 @@ function exec_with_version() {
819
966
  #
820
967
 
821
968
  function is_ok() {
822
- # Note: both curl and wget can follow redirects, as present on some mirrors (e.g. https://npm.taobao.org/mirrors/node).
823
- # The output is complicated with redirects, so keep it simple and use command status rather than parse output.
824
- if command -v curl &> /dev/null; then
825
- do_get --silent --head "$1" > /dev/null || return 1
826
- else
827
- do_get --spider "$1" > /dev/null || return 1
828
- fi
969
+ # Both curl and wget follow redirects, which GitHub answers release downloads with.
970
+ fetch head "$1"
829
971
  }
830
972
 
831
973
  #
@@ -1044,7 +1186,7 @@ function get_latest_resolved_version() {
1044
1186
  display_remote_index() {
1045
1187
  local index_url="${g_mirror_url}/index.tab"
1046
1188
  # tail to remove header line
1047
- do_get_index "${index_url}" | tail -n +2 | cut -f 1,3,10
1189
+ fetch text "${index_url}" | tail -n +2 | cut -f 1,3,10
1048
1190
  if [[ "${PIPESTATUS[0]}" -ne 0 ]]; then
1049
1191
  # Reminder: abort will only exit subshell, but consistent error display
1050
1192
  abort "failed to download version index (${index_url})"
@@ -1132,7 +1274,7 @@ function display_remote_versions() {
1132
1274
  else [0, 0, 0, 0, 0, []]
1133
1275
  end;'
1134
1276
 
1135
- do_get_index "${index_url}" \
1277
+ fetch text "${index_url}" \
1136
1278
  | jq -r "${jq_semver_key} [ ${jq_release_filter} | .name ] | sort_by(semver_key) | reverse | .[]" \
1137
1279
  | n_grep -E "${match}" \
1138
1280
  | awk "NR<=${match_count}" \
@@ -1237,6 +1379,7 @@ function show_diagnostics() {
1237
1379
  echo "install destination: ${BE_PREFIX}"
1238
1380
  [[ -n "${BE_PREFIX}" ]] && echo "PATH: ${PATH}"
1239
1381
  echo "ls-remote max matches: ${BE_MAX_REMOTE_MATCHES}"
1382
+ echo "download attempts: ${BE_DOWNLOAD_ATTEMPTS}, the first retry after ${BE_DOWNLOAD_RETRY_DELAY} seconds"
1240
1383
 
1241
1384
  printf "\nProxy\n"
1242
1385
  # disable "var is referenced but not assigned": https://github.com/koalaman/shellcheck/wiki/SC2154
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beplus/be",
3
3
  "description": "Interactively Manage beplus CLI Versions",
4
- "version": "0.11.0",
4
+ "version": "0.12.0",
5
5
  "author": "Igor Lamos <igor@be.plus>",
6
6
  "homepage": "https://github.com/beplus/be",
7
7
  "bugs": "https://github.com/beplus/be",