Skip to content

Commit bd1d3ff

Browse files
authored
fix(install): allow interactive sudo from terminal installs
1 parent de638f5 commit bd1d3ff

3 files changed

Lines changed: 210 additions & 1 deletion

File tree

.github/workflows/install-scripts.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ on:
77
paths:
88
- install.sh
99
- install.ps1
10+
- tests/install_sh_sudo_test.sh
1011
- .github/workflows/install-scripts.yml
1112
pull_request:
1213
paths:
1314
- install.sh
1415
- install.ps1
16+
- tests/install_sh_sudo_test.sh
1517
- .github/workflows/install-scripts.yml
1618

1719
permissions:
@@ -29,6 +31,8 @@ jobs:
2931
run: sh -n install.sh
3032
- name: bash parse
3133
run: bash -n install.sh
34+
- name: installer behavior tests
35+
run: sh tests/install_sh_sudo_test.sh
3236

3337
mirror:
3438
name: mirror install scripts

install.sh

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ need_cmd() {
4848
fi
4949
}
5050

51+
can_prompt_for_sudo() {
52+
command -v sudo > /dev/null 2>&1 || return 1
53+
# `curl | sh` leaves stdin as the script pipe, but sudo can still prompt via
54+
# the controlling terminal. In CI/agent contexts stderr is usually not a TTY,
55+
# so do not risk an unanswerable password prompt there.
56+
[ -t 2 ] || return 1
57+
[ -r /dev/tty ] || return 1
58+
}
59+
5160
sha256_of() {
5261
file="$1"
5362
if command -v sha256sum > /dev/null 2>&1; then
@@ -298,7 +307,11 @@ main() {
298307
chmod +x "${INSTALL_DIR}/${INSTALLED_NAME}"
299308
elif sudo -n true 2>/dev/null; then
300309
# Passwordless sudo is available — install to the privileged dir without
301-
# prompting (a prompt would hang `curl | sh` in agents/CI: no TTY to answer).
310+
# prompting.
311+
sudo mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${INSTALLED_NAME}"
312+
sudo chmod +x "${INSTALL_DIR}/${INSTALLED_NAME}"
313+
elif can_prompt_for_sudo; then
314+
info "Need elevated permissions to install to ${INSTALL_DIR}"
302315
sudo mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${INSTALLED_NAME}"
303316
sudo chmod +x "${INSTALL_DIR}/${INSTALLED_NAME}"
304317
else

tests/install_sh_sudo_test.sh

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5+
TMP_DIR=$(mktemp -d)
6+
cleanup() {
7+
chmod -R u+w "${TMP_DIR}" 2>/dev/null || true
8+
rm -rf "${TMP_DIR}"
9+
}
10+
trap cleanup EXIT
11+
12+
FAKE_BIN="${TMP_DIR}/bin"
13+
FIXTURES="${TMP_DIR}/fixtures"
14+
mkdir -p "${FAKE_BIN}" "${FIXTURES}"
15+
16+
ARCHIVE="flashduty-cli_Linux_x86_64.tar.gz"
17+
18+
make_fixtures() {
19+
cli_dir="${TMP_DIR}/cli"
20+
mkdir -p "${cli_dir}"
21+
cat > "${cli_dir}/flashduty-cli" <<'EOS'
22+
#!/bin/sh
23+
echo "fake flashduty $*"
24+
EOS
25+
chmod +x "${cli_dir}/flashduty-cli"
26+
(cd "${cli_dir}" && tar czf "${FIXTURES}/${ARCHIVE}" flashduty-cli)
27+
if command -v sha256sum >/dev/null 2>&1; then
28+
sum=$(sha256sum "${FIXTURES}/${ARCHIVE}" | awk '{print $1}')
29+
else
30+
sum=$(shasum -a 256 "${FIXTURES}/${ARCHIVE}" | awk '{print $1}')
31+
fi
32+
printf '%s %s\n' "${sum}" "${ARCHIVE}" > "${FIXTURES}/checksums.txt"
33+
}
34+
35+
make_fake_commands() {
36+
cat > "${FAKE_BIN}/uname" <<'EOS'
37+
#!/bin/sh
38+
case "$1" in
39+
-s) echo Linux ;;
40+
-m) echo x86_64 ;;
41+
*) /usr/bin/uname "$@" ;;
42+
esac
43+
EOS
44+
chmod +x "${FAKE_BIN}/uname"
45+
46+
cat > "${FAKE_BIN}/curl" <<EOS
47+
#!/bin/sh
48+
out=""
49+
url=""
50+
while [ "\$#" -gt 0 ]; do
51+
case "\$1" in
52+
--proto) shift 2 ;;
53+
--tlsv1.2|-fsSL) shift ;;
54+
-o) out="\$2"; shift 2 ;;
55+
-*) shift ;;
56+
*) url="\$1"; shift ;;
57+
esac
58+
done
59+
60+
case "\${url}" in
61+
*/${ARCHIVE}) src="${FIXTURES}/${ARCHIVE}" ;;
62+
*/checksums.txt) src="${FIXTURES}/checksums.txt" ;;
63+
*) echo "unexpected curl URL: \${url}" >&2; exit 22 ;;
64+
esac
65+
66+
if [ -n "\${out}" ]; then
67+
cp "\${src}" "\${out}"
68+
else
69+
cat "\${src}"
70+
fi
71+
EOS
72+
chmod +x "${FAKE_BIN}/curl"
73+
74+
cat > "${FAKE_BIN}/sudo" <<'EOS'
75+
#!/bin/sh
76+
printf '%s\n' "sudo $*" >> "${SUDO_LOG}"
77+
if [ "${1:-}" = "-n" ] && [ "${2:-}" = "true" ]; then
78+
exit 1
79+
fi
80+
if [ "${1:-}" = "mv" ]; then
81+
dir=$(dirname -- "$3")
82+
chmod u+w "${dir}" 2>/dev/null || true
83+
mv "$2" "$3"
84+
chmod a-w "${dir}" 2>/dev/null || true
85+
exit 0
86+
fi
87+
exec "$@"
88+
EOS
89+
chmod +x "${FAKE_BIN}/sudo"
90+
}
91+
92+
run_install() {
93+
home_dir="$1"
94+
install_dir="$2"
95+
out_file="$3"
96+
env PATH="${FAKE_BIN}:$PATH" \
97+
HOME="${home_dir}" \
98+
SHELL=/bin/false \
99+
FLASHDUTY_VERSION=v9.9.9 \
100+
MIRROR_URL=https://mirror.example/flashduty-cli \
101+
FLASHDUTY_INSTALL_DIR="${install_dir}" \
102+
SUDO_LOG="${SUDO_LOG}" \
103+
sh < "${ROOT}/install.sh" > "${out_file}" 2>&1
104+
}
105+
106+
run_install_with_tty() {
107+
home_dir="$1"
108+
install_dir="$2"
109+
out_file="$3"
110+
runner="${TMP_DIR}/run-install-with-tty.sh"
111+
cat > "${runner}" <<EOS
112+
#!/bin/sh
113+
exec env PATH="${FAKE_BIN}:\$PATH" \\
114+
HOME="${home_dir}" \\
115+
SHELL=/bin/false \\
116+
FLASHDUTY_VERSION=v9.9.9 \\
117+
MIRROR_URL=https://mirror.example/flashduty-cli \\
118+
FLASHDUTY_INSTALL_DIR="${install_dir}" \\
119+
SUDO_LOG="${SUDO_LOG}" \\
120+
sh < "${ROOT}/install.sh"
121+
EOS
122+
chmod +x "${runner}"
123+
124+
if script --version >/dev/null 2>&1; then
125+
script -q -e -c "${runner}" /dev/null > "${out_file}" 2>&1
126+
else
127+
script -q /dev/null "${runner}" > "${out_file}" 2>&1
128+
fi
129+
}
130+
131+
assert_file_exists() {
132+
if [ ! -e "$1" ]; then
133+
echo "expected file to exist: $1" >&2
134+
exit 1
135+
fi
136+
}
137+
138+
assert_file_missing() {
139+
if [ -e "$1" ]; then
140+
echo "expected file to be absent: $1" >&2
141+
exit 1
142+
fi
143+
}
144+
145+
assert_contains() {
146+
if ! grep -Fq "$2" "$1"; then
147+
echo "expected $1 to contain: $2" >&2
148+
echo "--- $1 ---" >&2
149+
cat "$1" >&2
150+
exit 1
151+
fi
152+
}
153+
154+
test_non_tty_falls_back_to_user_bin() {
155+
SUDO_LOG="${TMP_DIR}/sudo-non-tty.log"
156+
export SUDO_LOG
157+
: > "${SUDO_LOG}"
158+
home_dir="${TMP_DIR}/home-non-tty"
159+
install_dir="${TMP_DIR}/system-non-tty"
160+
out_file="${TMP_DIR}/non-tty.out"
161+
mkdir -p "${home_dir}" "${install_dir}"
162+
chmod 555 "${install_dir}"
163+
164+
run_install "${home_dir}" "${install_dir}" "${out_file}"
165+
166+
assert_contains "${out_file}" "Install dir not writable and no passwordless sudo; installing to ${home_dir}/.local/bin"
167+
assert_file_exists "${home_dir}/.local/bin/flashduty"
168+
assert_file_missing "${install_dir}/flashduty"
169+
}
170+
171+
test_tty_prompts_for_interactive_sudo() {
172+
SUDO_LOG="${TMP_DIR}/sudo-tty.log"
173+
export SUDO_LOG
174+
: > "${SUDO_LOG}"
175+
home_dir="${TMP_DIR}/home-tty"
176+
install_dir="${TMP_DIR}/system-tty"
177+
out_file="${TMP_DIR}/tty.out"
178+
mkdir -p "${home_dir}" "${install_dir}"
179+
chmod 555 "${install_dir}"
180+
181+
run_install_with_tty "${home_dir}" "${install_dir}" "${out_file}"
182+
183+
assert_contains "${out_file}" "Need elevated permissions to install to ${install_dir}"
184+
assert_contains "${SUDO_LOG}" "sudo mv"
185+
assert_file_exists "${install_dir}/flashduty"
186+
assert_file_missing "${home_dir}/.local/bin/flashduty"
187+
}
188+
189+
make_fixtures
190+
make_fake_commands
191+
test_non_tty_falls_back_to_user_bin
192+
test_tty_prompts_for_interactive_sudo

0 commit comments

Comments
 (0)