Skip to content

Commit 0c07b82

Browse files
committed
Bound the prompt hook's cost, and add a kill switch
- Read the typescript bounded (first 64K + last 1M) instead of the whole file into a variable, so the prompt after a failed command no longer scales with how much it printed: 35 MB took 2.4 s, now 0.1 s, and 350 MB would have taken 24 s. - Run each helper under timeout (5 s, then SIGKILL), so a slow or stuck helper cannot stall the prompt; today's helpers can't block, but the framework accepts helpers in any language. - HELP50_DISABLED in the environment disables help50 at login and is reported by help50 is-enabled/status. Set as an organization-wide Codespaces secret, it turns help50 off for everyone at their next login without rebuilding an image; set by one user, it's a persistent personal opt-out. Smoke tests cover all three.
1 parent ba541a2 commit 0c07b82

3 files changed

Lines changed: 55 additions & 5 deletions

File tree

‎etc/profile.d/help50.sh‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,15 @@ function _help50() {
5555
# https://tldp.org/LDP/abs/html/exitcodes.html
5656
if [[ $status -ne 0 && $status -ne 130 && $status -ne 148 ]]; then
5757

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

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

113-
# Try to get help
120+
# Try to get help, giving each helper a few seconds at most, lest a slow or stuck helper stall the prompt
114121
for helper in $HELPERS/*; do
115122
if [[ -f $helper && -x $helper ]]; then
116-
local help=$($helper $argv <<< "$typescript")
123+
local help=$(timeout -k 1 5 $helper $argv <<< "$typescript")
117124
if [[ -n "$help" ]]; then
118125
break
119126
fi

‎opt/cs50/bin/help50‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ function _disable() {
55
}
66

77
function _is-enabled() {
8-
if [[ -f /tmp/help50.lock ]]; then
8+
9+
# Kill switch: set in the environment (e.g., an organization-wide Codespaces secret) to
10+
# turn help50 off for everyone at their next login, without rebuilding an image
11+
if [[ -n "$HELP50_DISABLED" ]]; then
12+
echo "disabled (HELP50_DISABLED is set)"
13+
return 1
14+
elif [[ -f /tmp/help50.lock ]]; then
915
echo disabled
1016
return 1
1117
else

‎tests/smoke.sh‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,43 @@ run "$IMAGE" bash --login -c '
5454
test "$(cat /tmp/cmd)" = ./slow || exit 1
5555
'
5656

57+
echo "- the prompt hook stays fast after a failed command printed a huge amount of output"
58+
run "$IMAGE" bash --login -c '
59+
export HELP50=$(mktemp)
60+
_helpless() { printf "%s" "$1" > /tmp/output; }
61+
. /etc/profile.d/help50.sh
62+
63+
# 35 MB (4 million lines) of output, then an error, as script(1) records it.
64+
# Reading the whole file into a variable took ~2.4 s here and scaled linearly.
65+
{ printf "$ ./huge\r\n"; seq 1 4000000 | sed "s/$/\r/"; printf "Error: boom\r\n"; } > "$HELP50"
66+
size=$(stat -c %s "$HELP50")
67+
set -o history; history -s ./huge; set +o history
68+
start=$(date +%s%N); false; _help50; elapsed=$(( ($(date +%s%N) - start) / 1000000 ))
69+
echo " hook took ${elapsed} ms for a ${size}-byte typescript"
70+
test "$elapsed" -lt 1000 &&
71+
test "$(tail -n 1 /tmp/output)" = "Error: boom" || exit 1
72+
'
73+
74+
echo "- a helper that hangs cannot stall the prompt"
75+
run --user root "$IMAGE" bash --login -c '
76+
printf "#!/bin/bash\ncat > /dev/null\nsleep 60\n" > /opt/cs50/lib/help50/zz_hang && chmod 755 /opt/cs50/lib/help50/zz_hang
77+
su ubuntu -c "bash --login -c '"'"'
78+
export HELP50=\$(mktemp); . /etc/profile.d/help50.sh
79+
printf \"\$ ./x\\r\\nsome error\\r\\n\" > \"\$HELP50\"
80+
set -o history; history -s ./x; set +o history
81+
start=\$(date +%s); false; _help50; elapsed=\$(( \$(date +%s) - start ))
82+
echo \"hook took \${elapsed} s with a hung helper\"; test \"\$elapsed\" -lt 15
83+
'"'"'"
84+
'
85+
86+
echo "- HELP50_DISABLED in the environment keeps help50 from starting, and says so"
87+
run "$IMAGE" bash --login -c 'help50 is-enabled | grep -qx enabled'
88+
run --env HELP50_DISABLED=1 "$IMAGE" bash --login -c '
89+
out=$(help50 is-enabled); test $? -eq 1 && [[ "$out" == *HELP50_DISABLED* ]] || exit 1'
90+
# In an interactive shell on a pty (script provides one), help50 starts by default but not when disabled
91+
run "$IMAGE" bash -c 'echo "help50 status; exit" | script -qc "bash --login -i" /dev/null' | grep -q '^started'
92+
run --env HELP50_DISABLED=1 "$IMAGE" bash -c 'echo "help50 status; exit" | script -qc "bash --login -i" /dev/null' | grep -q '^stopped'
93+
5794
echo "- help50 COMMAND runs COMMAND, with its exit status"
5895
run "$IMAGE" bash --login -c 'help50 true && ! help50 false && test "$(help50 echo x)" = x'
5996
run "$IMAGE" bash --login -c 'help50 valgrind python x.py < /dev/null; test $? -eq 1' 2>&1 | grep -q 'does not support Python'

0 commit comments

Comments
 (0)