diff --git a/install.sh b/install.sh index 796b7557..38ce997f 100755 --- a/install.sh +++ b/install.sh @@ -272,26 +272,50 @@ install_nvm_as_script() { else nvm_echo "=> Downloading nvm as script to '$INSTALL_DIR'" fi - nvm_download -s "$NVM_SOURCE_LOCAL" -o "$INSTALL_DIR/nvm.sh" || { + # download to temporary names, and only move them into place once every download has succeeded, + # so that a failed download never replaces a working install with a partial or empty file + nvm_download -s "$NVM_SOURCE_LOCAL" -o "$INSTALL_DIR/nvm.sh.download" || { nvm_echo >&2 "Failed to download '$NVM_SOURCE_LOCAL'" return 1 } & - nvm_download -s "$NVM_EXEC_SOURCE" -o "$INSTALL_DIR/nvm-exec" || { + local NVM_SH_PID + NVM_SH_PID=$! + nvm_download -s "$NVM_EXEC_SOURCE" -o "$INSTALL_DIR/nvm-exec.download" || { nvm_echo >&2 "Failed to download '$NVM_EXEC_SOURCE'" return 2 } & - nvm_download -s "$NVM_BASH_COMPLETION_SOURCE" -o "$INSTALL_DIR/bash_completion" || { + local NVM_EXEC_PID + NVM_EXEC_PID=$! + nvm_download -s "$NVM_BASH_COMPLETION_SOURCE" -o "$INSTALL_DIR/bash_completion.download" || { nvm_echo >&2 "Failed to download '$NVM_BASH_COMPLETION_SOURCE'" return 2 } & - for job in $(jobs -p | command sort) + local NVM_BASH_COMPLETION_PID + NVM_BASH_COMPLETION_PID=$! + # wait for every download, so that none is still writing when the partial files are removed + local NVM_DOWNLOAD_STATUS + NVM_DOWNLOAD_STATUS=0 + local job + for job in "$NVM_SH_PID" "$NVM_EXEC_PID" "$NVM_BASH_COMPLETION_PID" do - wait "$job" || return $? + wait "$job" || NVM_DOWNLOAD_STATUS=$? done - chmod a+x "$INSTALL_DIR/nvm-exec" || { + if [ "$NVM_DOWNLOAD_STATUS" -ne 0 ]; then + command rm -f "$INSTALL_DIR/nvm.sh.download" "$INSTALL_DIR/nvm-exec.download" "$INSTALL_DIR/bash_completion.download" + return "$NVM_DOWNLOAD_STATUS" + fi + chmod a+x "$INSTALL_DIR/nvm-exec.download" || { nvm_echo >&2 "Failed to mark '$INSTALL_DIR/nvm-exec' as executable" return 3 } + local NVM_SCRIPT_FILE + for NVM_SCRIPT_FILE in nvm-exec bash_completion nvm.sh + do + command mv -f "$INSTALL_DIR/$NVM_SCRIPT_FILE.download" "$INSTALL_DIR/$NVM_SCRIPT_FILE" || { + nvm_echo >&2 "Failed to move '$INSTALL_DIR/$NVM_SCRIPT_FILE.download' into place" + return 3 + } + done } nvm_try_profile() { @@ -428,7 +452,7 @@ nvm_do_install() { if nvm_has git; then install_nvm_from_git elif nvm_has_executable curl || nvm_has_executable wget; then - install_nvm_as_script + install_nvm_as_script || exit $? else nvm_echo >&2 'You need git, curl, or wget to install nvm' exit 1 @@ -444,7 +468,7 @@ nvm_do_install() { nvm_echo >&2 "You need curl or wget to install nvm" exit 1 fi - install_nvm_as_script + install_nvm_as_script || exit $? else nvm_echo >&2 "The environment variable \$METHOD is set to \"${METHOD}\", which is not recognized as a valid installation method." exit 1 diff --git a/test/install_script/install_nvm_as_script_download_failure b/test/install_script/install_nvm_as_script_download_failure new file mode 100755 index 00000000..6ecbb41c --- /dev/null +++ b/test/install_script/install_nvm_as_script_download_failure @@ -0,0 +1,71 @@ +#!/bin/sh + +cleanup () { + NVM_DIR="$SAVE_NVM_DIR" + if [ -n "${TEST_DIR-}" ]; then + rm -rf "${TEST_DIR}" + fi + unset SAVE_NVM_DIR TEST_DIR OUTPUT EXIT_CODE FILE + unset -f die cleanup nvm_download install_nvm_as_script +} +die () { echo "$@" ; cleanup ; exit 1; } + +SAVE_NVM_DIR="$NVM_DIR" + +NVM_ENV=testing \. ../../install.sh + +type install_nvm_as_script > /dev/null 2>&1 || die 'install_nvm_as_script is not available' + +TEST_DIR="$(mktemp -d)" +NVM_DIR="${TEST_DIR}/nvm" +mkdir -p "${NVM_DIR}" +echo 'working nvm.sh' > "${NVM_DIR}/nvm.sh" + +# like curl, write part of the file and then fail, as when the connection drops part-way through nvm.sh +nvm_download() { + case "${2}" in + */nvm.sh) + echo 'partial' > "${4}" + return 56 + ;; + esac + echo "${2}" > "${4}" +} + +## install_nvm_as_script: a failed download fails, and leaves the existing install untouched +OUTPUT="$(install_nvm_as_script 2>&1)" +EXIT_CODE=$? +[ "${EXIT_CODE}" != '0' ] || die "install_nvm_as_script should fail when a download fails, got ${EXIT_CODE}" +echo "${OUTPUT}" | grep -q "Failed to download" || die "install_nvm_as_script should print the download error, got: ${OUTPUT}" +[ "$(cat "${NVM_DIR}/nvm.sh")" = 'working nvm.sh' ] || die "a failed download replaced the working nvm.sh with: $(cat "${NVM_DIR}/nvm.sh")" +[ ! -e "${NVM_DIR}/nvm-exec" ] || die 'nvm-exec was installed although the nvm.sh download failed' +[ ! -e "${NVM_DIR}/bash_completion" ] || die 'bash_completion was installed although the nvm.sh download failed' +for FILE in nvm.sh nvm-exec bash_completion; do + [ ! -e "${NVM_DIR}/${FILE}.download" ] || die "a failed download left '${FILE}.download' behind" +done + +## install_nvm_as_script: when every download succeeds, all three files are installed +nvm_download() { + echo "${2}" > "${4}" +} +OUTPUT="$(install_nvm_as_script 2>&1)" +EXIT_CODE=$? +[ "${EXIT_CODE}" = '0' ] || die "install_nvm_as_script should succeed when every download succeeds, got ${EXIT_CODE}: ${OUTPUT}" +[ "$(cat "${NVM_DIR}/nvm.sh")" = "$(nvm_source script)" ] || die "nvm.sh was not updated, got: $(cat "${NVM_DIR}/nvm.sh")" +[ "$(cat "${NVM_DIR}/nvm-exec")" = "$(nvm_source script-nvm-exec)" ] || die "nvm-exec was not installed, got: $(cat "${NVM_DIR}/nvm-exec")" +[ "$(cat "${NVM_DIR}/bash_completion")" = "$(nvm_source script-nvm-bash-completion)" ] || die "bash_completion was not installed, got: $(cat "${NVM_DIR}/bash_completion")" +[ -x "${NVM_DIR}/nvm-exec" ] || die 'nvm-exec was not marked executable' +for FILE in nvm.sh nvm-exec bash_completion; do + [ ! -e "${NVM_DIR}/${FILE}.download" ] || die "a successful download left '${FILE}.download' behind" +done + +## nvm_do_install: a failed script install exits non-zero, before touching the profile or sourcing nvm.sh +install_nvm_as_script() { + return 2 +} +OUTPUT="$(METHOD=script PROFILE=/dev/null nvm_do_install 2>&1)" +EXIT_CODE=$? +[ "${EXIT_CODE}" = '2' ] || die "nvm_do_install should exit 2 when install_nvm_as_script fails, got ${EXIT_CODE}: ${OUTPUT}" +! echo "${OUTPUT}" | grep -q 'Close and reopen your terminal' || die "nvm_do_install reported success although install_nvm_as_script failed: ${OUTPUT}" + +cleanup