From 08eb8da6a49c2ff31dd427746ac1f39959731983 Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Wed, 10 Jun 2026 17:51:07 +0800 Subject: [PATCH] fix(install): allow interactive sudo from terminal installs --- .github/workflows/install-scripts.yml | 4 + install.sh | 15 +- tests/install_sh_sudo_test.sh | 192 ++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 tests/install_sh_sudo_test.sh diff --git a/.github/workflows/install-scripts.yml b/.github/workflows/install-scripts.yml index 4a8d2eb..77a5082 100644 --- a/.github/workflows/install-scripts.yml +++ b/.github/workflows/install-scripts.yml @@ -7,11 +7,13 @@ on: paths: - install.sh - install.ps1 + - tests/install_sh_sudo_test.sh - .github/workflows/install-scripts.yml pull_request: paths: - install.sh - install.ps1 + - tests/install_sh_sudo_test.sh - .github/workflows/install-scripts.yml permissions: @@ -29,6 +31,8 @@ jobs: run: sh -n install.sh - name: bash parse run: bash -n install.sh + - name: installer behavior tests + run: sh tests/install_sh_sudo_test.sh mirror: name: mirror install scripts diff --git a/install.sh b/install.sh index 169ed8c..c3d1b6e 100755 --- a/install.sh +++ b/install.sh @@ -48,6 +48,15 @@ need_cmd() { fi } +can_prompt_for_sudo() { + command -v sudo > /dev/null 2>&1 || return 1 + # `curl | sh` leaves stdin as the script pipe, but sudo can still prompt via + # the controlling terminal. In CI/agent contexts stderr is usually not a TTY, + # so do not risk an unanswerable password prompt there. + [ -t 2 ] || return 1 + [ -r /dev/tty ] || return 1 +} + sha256_of() { file="$1" if command -v sha256sum > /dev/null 2>&1; then @@ -298,7 +307,11 @@ main() { chmod +x "${INSTALL_DIR}/${INSTALLED_NAME}" elif sudo -n true 2>/dev/null; then # Passwordless sudo is available — install to the privileged dir without - # prompting (a prompt would hang `curl | sh` in agents/CI: no TTY to answer). + # prompting. + sudo mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${INSTALLED_NAME}" + sudo chmod +x "${INSTALL_DIR}/${INSTALLED_NAME}" + elif can_prompt_for_sudo; then + info "Need elevated permissions to install to ${INSTALL_DIR}" sudo mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${INSTALLED_NAME}" sudo chmod +x "${INSTALL_DIR}/${INSTALLED_NAME}" else diff --git a/tests/install_sh_sudo_test.sh b/tests/install_sh_sudo_test.sh new file mode 100644 index 0000000..b1a0428 --- /dev/null +++ b/tests/install_sh_sudo_test.sh @@ -0,0 +1,192 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +TMP_DIR=$(mktemp -d) +cleanup() { + chmod -R u+w "${TMP_DIR}" 2>/dev/null || true + rm -rf "${TMP_DIR}" +} +trap cleanup EXIT + +FAKE_BIN="${TMP_DIR}/bin" +FIXTURES="${TMP_DIR}/fixtures" +mkdir -p "${FAKE_BIN}" "${FIXTURES}" + +ARCHIVE="flashduty-cli_Linux_x86_64.tar.gz" + +make_fixtures() { + cli_dir="${TMP_DIR}/cli" + mkdir -p "${cli_dir}" + cat > "${cli_dir}/flashduty-cli" <<'EOS' +#!/bin/sh +echo "fake flashduty $*" +EOS + chmod +x "${cli_dir}/flashduty-cli" + (cd "${cli_dir}" && tar czf "${FIXTURES}/${ARCHIVE}" flashduty-cli) + if command -v sha256sum >/dev/null 2>&1; then + sum=$(sha256sum "${FIXTURES}/${ARCHIVE}" | awk '{print $1}') + else + sum=$(shasum -a 256 "${FIXTURES}/${ARCHIVE}" | awk '{print $1}') + fi + printf '%s %s\n' "${sum}" "${ARCHIVE}" > "${FIXTURES}/checksums.txt" +} + +make_fake_commands() { + cat > "${FAKE_BIN}/uname" <<'EOS' +#!/bin/sh +case "$1" in + -s) echo Linux ;; + -m) echo x86_64 ;; + *) /usr/bin/uname "$@" ;; +esac +EOS + chmod +x "${FAKE_BIN}/uname" + + cat > "${FAKE_BIN}/curl" <&2; exit 22 ;; +esac + +if [ -n "\${out}" ]; then + cp "\${src}" "\${out}" +else + cat "\${src}" +fi +EOS + chmod +x "${FAKE_BIN}/curl" + + cat > "${FAKE_BIN}/sudo" <<'EOS' +#!/bin/sh +printf '%s\n' "sudo $*" >> "${SUDO_LOG}" +if [ "${1:-}" = "-n" ] && [ "${2:-}" = "true" ]; then + exit 1 +fi +if [ "${1:-}" = "mv" ]; then + dir=$(dirname -- "$3") + chmod u+w "${dir}" 2>/dev/null || true + mv "$2" "$3" + chmod a-w "${dir}" 2>/dev/null || true + exit 0 +fi +exec "$@" +EOS + chmod +x "${FAKE_BIN}/sudo" +} + +run_install() { + home_dir="$1" + install_dir="$2" + out_file="$3" + env PATH="${FAKE_BIN}:$PATH" \ + HOME="${home_dir}" \ + SHELL=/bin/false \ + FLASHDUTY_VERSION=v9.9.9 \ + MIRROR_URL=https://mirror.example/flashduty-cli \ + FLASHDUTY_INSTALL_DIR="${install_dir}" \ + SUDO_LOG="${SUDO_LOG}" \ + sh < "${ROOT}/install.sh" > "${out_file}" 2>&1 +} + +run_install_with_tty() { + home_dir="$1" + install_dir="$2" + out_file="$3" + runner="${TMP_DIR}/run-install-with-tty.sh" + cat > "${runner}" </dev/null 2>&1; then + script -q -e -c "${runner}" /dev/null > "${out_file}" 2>&1 + else + script -q /dev/null "${runner}" > "${out_file}" 2>&1 + fi +} + +assert_file_exists() { + if [ ! -e "$1" ]; then + echo "expected file to exist: $1" >&2 + exit 1 + fi +} + +assert_file_missing() { + if [ -e "$1" ]; then + echo "expected file to be absent: $1" >&2 + exit 1 + fi +} + +assert_contains() { + if ! grep -Fq "$2" "$1"; then + echo "expected $1 to contain: $2" >&2 + echo "--- $1 ---" >&2 + cat "$1" >&2 + exit 1 + fi +} + +test_non_tty_falls_back_to_user_bin() { + SUDO_LOG="${TMP_DIR}/sudo-non-tty.log" + export SUDO_LOG + : > "${SUDO_LOG}" + home_dir="${TMP_DIR}/home-non-tty" + install_dir="${TMP_DIR}/system-non-tty" + out_file="${TMP_DIR}/non-tty.out" + mkdir -p "${home_dir}" "${install_dir}" + chmod 555 "${install_dir}" + + run_install "${home_dir}" "${install_dir}" "${out_file}" + + assert_contains "${out_file}" "Install dir not writable and no passwordless sudo; installing to ${home_dir}/.local/bin" + assert_file_exists "${home_dir}/.local/bin/flashduty" + assert_file_missing "${install_dir}/flashduty" +} + +test_tty_prompts_for_interactive_sudo() { + SUDO_LOG="${TMP_DIR}/sudo-tty.log" + export SUDO_LOG + : > "${SUDO_LOG}" + home_dir="${TMP_DIR}/home-tty" + install_dir="${TMP_DIR}/system-tty" + out_file="${TMP_DIR}/tty.out" + mkdir -p "${home_dir}" "${install_dir}" + chmod 555 "${install_dir}" + + run_install_with_tty "${home_dir}" "${install_dir}" "${out_file}" + + assert_contains "${out_file}" "Need elevated permissions to install to ${install_dir}" + assert_contains "${SUDO_LOG}" "sudo mv" + assert_file_exists "${install_dir}/flashduty" + assert_file_missing "${home_dir}/.local/bin/flashduty" +} + +make_fixtures +make_fake_commands +test_non_tty_falls_back_to_user_bin +test_tty_prompts_for_interactive_sudo