Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions etc/profile.d/help50.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,15 @@ function _help50() {
# https://tldp.org/LDP/abs/html/exitcodes.html
if [[ $status -ne 0 && $status -ne 130 && $status -ne 148 ]]; then

# Read typescript from disk
local typescript=$(cat $HELP50)
# Read typescript from disk, bounded: at most the first 64K (where the command line is
# echoed) and the last 1M (where errors tend to be), so that a program that printed a
# great deal before failing doesn't stall the prompt while the whole file is read
local typescript
if [[ $(stat -c %s "$HELP50" 2> /dev/null || echo 0) -gt $((65536 + 1048576)) ]]; then
typescript=$(head -c 65536 "$HELP50"; echo; echo "[... output omitted ...]"; tail -c 1048576 "$HELP50")
else
typescript=$(cat "$HELP50")
fi

# Remove script's own output (if this is user's first command)
typescript=$(echo "$typescript" | sed '1{/^Script started on .*/d}')
Expand Down Expand Up @@ -110,10 +117,14 @@ function _help50() {
typescript="$after_first"
fi

# Try to get help
# Try to get help, giving each helper a few seconds at most, lest a slow or stuck helper stall
# the prompt. Note that timeout runs the helper in its own process group, so ctl-c at the
# terminal no longer reaches the helper (it did before); the timeout itself is the bound.
# Not --foreground, which would restore ctl-c but stop timeout from killing the helper's
# children, so an orphaned child holding stdout open could stall the prompt indefinitely.
for helper in $HELPERS/*; do
if [[ -f $helper && -x $helper ]]; then
local help=$($helper $argv <<< "$typescript")
local help=$(timeout -k 1 5 $helper $argv <<< "$typescript")
if [[ -n "$help" ]]; then
break
fi
Expand Down
12 changes: 12 additions & 0 deletions opt/cs50/bin/help50
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ function _disable() {
}

function _is-enabled() {

# Kill switch: set in the environment (e.g., an organization-wide Codespaces secret) to
# turn help50 off for everyone at their next login, without rebuilding an image. Values
# that read as false (0, false, no, off) count as unset, so that setting the secret to 0
# re-enables help50 just as deleting it would, rather than silently keeping it off
case "${HELP50_DISABLED,,}" in
""|0|false|no|off) ;;
*)
echo "disabled (HELP50_DISABLED=$HELP50_DISABLED; unset it to re-enable)"
return 1
;;
esac
if [[ -f /tmp/help50.lock ]]; then
echo disabled
return 1
Expand Down
43 changes: 43 additions & 0 deletions tests/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,49 @@ run "$IMAGE" bash --login -c '
test "$(cat /tmp/cmd)" = ./slow || exit 1
'

echo "- the prompt hook stays fast after a failed command printed a huge amount of output"
run "$IMAGE" bash --login -c '
export HELP50=$(mktemp)
_helpless() { printf "%s" "$1" > /tmp/output; }
. /etc/profile.d/help50.sh

# 35 MB (4 million lines) of output, then an error, as script(1) records it.
# Reading the whole file into a variable took ~2.4 s here and scaled linearly.
{ printf "$ ./huge\r\n"; seq 1 4000000 | sed "s/$/\r/"; printf "Error: boom\r\n"; } > "$HELP50"
size=$(stat -c %s "$HELP50")
set -o history; history -s ./huge; set +o history
start=$(date +%s%N); false; _help50; elapsed=$(( ($(date +%s%N) - start) / 1000000 ))
echo " hook took ${elapsed} ms for a ${size}-byte typescript"
test "$elapsed" -lt 1000 &&
test "$(tail -n 1 /tmp/output)" = "Error: boom" || exit 1
'

echo "- a helper that hangs cannot stall the prompt"
run --user root "$IMAGE" bash --login -c '
printf "#!/bin/bash\ncat > /dev/null\nsleep 60\n" > /opt/cs50/lib/help50/zz_hang && chmod 755 /opt/cs50/lib/help50/zz_hang
su ubuntu -c "bash --login -c '"'"'
export HELP50=\$(mktemp); . /etc/profile.d/help50.sh
printf \"\$ ./x\\r\\nsome error\\r\\n\" > \"\$HELP50\"
set -o history; history -s ./x; set +o history
start=\$(date +%s); false; _help50; elapsed=\$(( \$(date +%s) - start ))
echo \"hook took \${elapsed} s with a hung helper\"; test \"\$elapsed\" -lt 15
'"'"'"
'

echo "- HELP50_DISABLED in the environment keeps help50 from starting, and says so"
run "$IMAGE" bash --login -c 'help50 is-enabled | grep -qx enabled'
run --env HELP50_DISABLED=1 "$IMAGE" bash --login -c '
out=$(help50 is-enabled); test $? -eq 1 && [[ "$out" == *HELP50_DISABLED=1* && "$out" == *unset* ]] || exit 1'
# Values that read as false count as unset, so that setting the secret to 0 re-enables help50, as deleting it would
for value in 0 false FALSE no off ""; do
run --env HELP50_DISABLED="$value" "$IMAGE" bash --login -c 'help50 is-enabled | grep -qx enabled'
done
# The lock file (help50 disable) is still honored when the environment doesn't disable
run --env HELP50_DISABLED=0 "$IMAGE" bash --login -c 'help50 disable && ! help50 is-enabled && help50 enable && help50 is-enabled' > /dev/null
# In an interactive shell on a pty (script provides one), help50 starts by default but not when disabled
run "$IMAGE" bash -c 'echo "help50 status; exit" | script -qc "bash --login -i" /dev/null' | grep -q '^started'
run --env HELP50_DISABLED=1 "$IMAGE" bash -c 'echo "help50 status; exit" | script -qc "bash --login -i" /dev/null' | grep -q '^stopped'

echo "- help50 COMMAND runs COMMAND, with its exit status"
run "$IMAGE" bash --login -c 'help50 true && ! help50 false && test "$(help50 echo x)" = x'
run "$IMAGE" bash --login -c 'help50 valgrind python x.py < /dev/null; test $? -eq 1' 2>&1 | grep -q 'does not support Python'
Expand Down
Loading