Skip to content

Commit 9a23f46

Browse files
authored
fix(install): read prompt from /dev/tty when stdin is piped (#42)
* fix(install): read prompt from /dev/tty when stdin is piped When the install script is run via the documented `curl ... | sh` pattern, stdin is the pipe carrying script bytes, so `read -r response` cannot capture keystrokes and the service-install prompt silently ignores keyboard input (reported by Ivvor on Matrix). A previous fix (eff317ae, Dec 2025) addressed this with `< /dev/tty`, but PRs #36 / #37 resynced this file from freenet-core and dropped the override. Reapply the fix and harden it for environments without a controlling terminal. The new logic: - If stdin is a TTY, read normally (covers `sh install.sh`). - Else, probe /dev/tty by actually opening it (`{ true </dev/tty; }`) rather than relying on `[ -r /dev/tty ]`, which can return true even when the open later fails. If /dev/tty is openable, read from it (covers `curl | sh`). - Otherwise, skip the prompt rather than aborting under `set -eu` (covers truly non-interactive shells, e.g. CI or `setsid`). Verified via a pty harness: `curl | sh` simulation under `script(1)` reads "y" from /dev/tty and proceeds; `setsid ... </dev/null` skips the prompt and prints follow-up instructions. Note: freenet-core/scripts/install.sh has the identical bug; a matching fix will follow there to keep the resync in sync. [AI-assisted - Claude] * fix(install): address review feedback - $0 detection, EOF guard Five-perspective review on PR #42 surfaced three issues with the initial fix: - Codex P2: discriminating only on `[ -t 0 ]` regresses `printf 'y' | sh install.sh` automation patterns. When stdin is piped but the script ran from a file, the user's answer is on stdin, not /dev/tty. - Skeptical #1: `read -r response` returning EOF (Ctrl-D, closed stdin) under `set -eu` aborts the script. - Skeptical #10: comment used an em-dash, which Ian's writing-style rule rejects. Redesigned the dispatch around `$0`: when sh runs a script file (file-execution), $0 is the script path; when sh reads its own source from stdin (`curl | sh`), $0 is the shell name. The case arm tests for known shell names and only redirects to /dev/tty in that branch. The file-execution arm reads from stdin as before, so piped automation still works. Both `read` calls now have `|| response=""` so EOF leaves the default-N case path intact rather than aborting. Also (lower priority feedback applied): - Skipped path now mentions FREENET_NO_SERVICE=1 as the documented way to bypass the prompt non-interactively (Skeptical #2/#3). - Removed the redundant "Non-interactive shell detected" info line (Skeptical #7); the existing `*)` case arm already prints "Skipping service installation" plus remediation guidance. - Added a NOTE block at the top of the file pointing future resync agents at the freenet-core sibling and explaining why the prompt block must not be flattened back to a plain `read` (Big-picture #5). Re-tested via the same pty harness across five scenarios: 1. `curl | sh` under pty: reads 'y' from /dev/tty, installs. 2. `sh install.sh` interactive: reads from stdin. 3. `printf 'y' | sh install.sh`: reads from piped stdin (regression test for Codex's concern). 4. `setsid ... </dev/null`: skips, prints FREENET_NO_SERVICE hint. 5. `sh install.sh </dev/null`: handles EOF without aborting. All five pass. [AI-assisted - Claude]
1 parent c034ae6 commit 9a23f46

1 file changed

Lines changed: 47 additions & 3 deletions

File tree

hugo-site/static/install.sh

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99
# FREENET_INSTALL_DIR - Installation directory (default: ~/.local/bin)
1010
# FREENET_NO_SERVICE - Set to 1 to skip service installation prompt
1111
# FREENET_VERSION - Specific version to install (default: latest)
12+
#
13+
# NOTE: A near-identical copy lives at freenet-core/scripts/install.sh.
14+
# Keep the two in sync. In particular, the service-install prompt
15+
# (look for "Would you like to install Freenet as a system service")
16+
# MUST keep its $0 + /dev/tty handling so that `curl | sh` users can
17+
# answer the prompt - see freenet/web PR #42 / freenet-core PR for
18+
# context. A previous fix was already lost once via a bulk resync
19+
# (web PRs #36/#37).
1220

1321
set -eu
1422

@@ -409,11 +417,45 @@ main() {
409417
print_path_instructions "$install_dir"
410418
fi
411419

412-
# Ask about service installation (unless FREENET_NO_SERVICE is set)
420+
# Ask about service installation (unless FREENET_NO_SERVICE is set).
421+
#
422+
# When the script is piped via `curl ... | sh`, sh reads its own script
423+
# source from stdin. A plain `read` at this point would consume bytes
424+
# of script source instead of capturing the user's answer, so we
425+
# redirect from /dev/tty in that case.
426+
#
427+
# We detect "sh is reading the script from stdin" via $0: when sh runs
428+
# a script file, $0 is the script path; when sh reads its source from
429+
# stdin, $0 is the shell name itself (e.g. "sh", "bash"). In the
430+
# script-file case we read from stdin as before, so existing
431+
# automation patterns like `printf 'y\n' | sh install.sh` still work.
432+
# The robust way to bypass the prompt in any invocation form is
433+
# FREENET_NO_SERVICE=1.
434+
#
435+
# `{ true </dev/tty; } 2>/dev/null` is used instead of `[ -r /dev/tty ]`:
436+
# the access check can succeed even when the process has no
437+
# controlling terminal and the subsequent open(2) fails with ENXIO.
438+
# Probe by actually opening /dev/tty.
439+
#
440+
# `read -r response || response=""` keeps EOF (e.g. Ctrl-D, closed
441+
# stdin) from aborting the script under `set -eu`.
413442
if [ "${FREENET_NO_SERVICE:-0}" != "1" ]; then
414443
echo ""
415-
printf "Would you like to install Freenet as a system service? [y/N] "
416-
read -r response
444+
response=""
445+
case "${0##*/}" in
446+
sh|bash|dash|ash|busybox|-sh|-bash|-dash|-ash)
447+
# Script source is on stdin (`curl | sh` form).
448+
if { true </dev/tty; } 2>/dev/null; then
449+
printf "Would you like to install Freenet as a system service? [y/N] "
450+
read -r response </dev/tty || response=""
451+
fi
452+
;;
453+
*)
454+
# Script ran as a file; stdin carries the user's answer.
455+
printf "Would you like to install Freenet as a system service? [y/N] "
456+
read -r response || response=""
457+
;;
458+
esac
417459
case "$response" in
418460
[yY]|[yY][eE][sS])
419461
info "Installing service..."
@@ -427,6 +469,8 @@ main() {
427469
echo ""
428470
echo "You can install the service later with:"
429471
echo " freenet service install"
472+
echo ""
473+
echo "To skip this prompt entirely in scripted installs, set FREENET_NO_SERVICE=1."
430474
;;
431475
esac
432476
fi

0 commit comments

Comments
 (0)