Bound the prompt hook's cost, and add a kill switch - #248
Merged
Merged
Conversation
- 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.
- HELP50_DISABLED=0 (or false, no, off, case-insensitively) now counts as unset, so that an admin who sets the org secret to 0 to turn help50 back on gets what they asked for, rather than every student staying disabled with no error. The is-enabled message now shows the value and says to unset it. Smoke tests cover the false-y values and that the lock file is still honored when the environment doesn't disable. - Note in the prompt hook that timeout runs each helper in its own process group, so ctl-c no longer reaches a stuck helper; the timeout itself is the bound. --foreground would restore ctl-c but stop timeout from killing the helper's children, which would give back the hang this is meant to remove.
rongxin-liu
added a commit
that referenced
this pull request
Sep 30, 2026
Reimplements help50 in Bash, running locally and automatically per login shell, without a server. Usage is inspired by systemctl: - help50 start/stop/status/enable/disable/is-enabled control a session that logs the shell's I/O via script to /tmp/help50.$PPID - help50 COMMAND [ARGS...] runs COMMAND as though typed directly, with the same exit status - HELP50_DISABLED in the environment is a kill switch, so that as a Codespaces secret help50 can be turned off fleet-wide without a rebuild /etc/profile.d/help50.sh installs a PROMPT_COMMAND hook that, after a failed command, strips the typescript of ANSI/control characters and terminal echo, bounds the read (first 64K + last 1M) and the output (first 64 + last 1,024 lines), and passes it to each executable helper in /opt/cs50/lib/help50/ under a 5-second timeout. Helper output is shown via _helpful; otherwise _helpless receives the output and command line (a no-op here, overridden in cs50/codespace to relay to the CS50 Duck). Also adds /opt/cs50/lib/cli helper functions (_alert, _ansi, _find, _fold, _sure) used by the make, sqlite3, http-server, and valgrind wrappers; helpers for bash, cd, clang, make, and python; tests/smoke.sh (make smoke), run in CI against each architecture's build before pushing to Docker Hub; and installs bsdextrautils, colorized-logs, file, expect, and fzf, dropping the Python help50 package. Squashed from 99 commits, including #244, #245, #246, #247, and #248. Co-authored-by: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pre-rollout safety pass. The question was: can help50 hang a terminal, and what turns it off?
Findings, measured in the image
_findover a 60,000-file workspaceOnly the last two needed changes. Everything else on the hang question checked out: the interactive-tty guard keeps non-interactive shells away from
script; a program that prints forever until Ctrl-C exits 130, which the hook skips.Changes
Bounded read.
_help50read the whole typescript into a variable ($(cat $HELP50)) before any cap, so a program that printed a lot and then crashed made the next prompt wait in proportion: 35 MB, 2.4 s; 350 MB would have been ~24 s, which a student experiences as a hang. Now it reads at most the first 64 KB (where the command line is echoed) and the last 1 MB (where the error is), with a marker between. Same 35 MB: 0.1 s, and constant in output size.Helper timeout. Each helper now runs under
timeout -k 1 5. Today's helpers can't block, but the framework accepts helpers in any language; this makes "a helper hangs the prompt" impossible rather than Ctrl-C-recoverable. One trade-off, noted in the code:timeoutruns the helper in its own process group, so Ctrl-C at the terminal no longer reaches a stuck helper the way it did before; the prompt instead returns when the timeout fires, at most ~6 s per helper.--foregroundwould restore Ctrl-C but would stoptimeoutfrom killing the helper's children, so an orphaned child holding stdout open could stall the prompt indefinitely, which is the hang this change removes.Kill switch.
HELP50_DISABLEDin the environment disables help50 at login;help50 is-enabledreportsdisabled (HELP50_DISABLED=1; unset it to re-enable). Values that read as false (0,false,no,off, any case) count as unset, so setting the secret to0turns help50 back on just as deleting it would, rather than silently keeping everyone disabled. Codespaces supports organization-wide Codespaces secrets (that is howCS50_TOKENarrives), so settingHELP50_DISABLED=1at the org turns help50 off for every student at their next codespace start, with no image rebuild, and deleting the secret (or setting it to0) turns it back on. Set as a user-level Codespaces secret, it is a persistent personal opt-out. (Anexportin~/.bashrcis too late:/etc/profile.d/cli.shchecksis-enabledbefore~/.bashrcis read;help50 disableis the in-codespace opt-out.) Until now the only fleet-level option was revert, rebuild, and wait for students to rebuild.Escape hatches after this PR
timeoutwithin ~6 s; Ctrl-C is not needed, and no longer reaches the helper (see above).help50 stop.help50 disable.HELP50_DISABLED=1; delete it or set it to0to turn help50 back on.Testing
tests/smoke.shgains three checks: the hook completes in under 1 s on a 35 MB typescript and still delivers the final error line; a helper that sleeps 60 s is cut off within 15 s;HELP50_DISABLED=1is reported byis-enabled(with the value and how to clear it) and, in an interactive login shell under a pty, leaveshelp50 statusatstopped(with the default case as a positive control). The kill switch check also covers the false-y values (0,false,FALSE,no,off, empty) reportingenabled, and thathelp50 disable's lock file is still honored when the environment does not disable. Full suite passes in CI on both amd64 and arm64 (the 35 MB hook check measured 137 ms on amd64; the hung helper was cut off at 5 s).