Adds new version of help50 - #210
Merged
Merged
Conversation
Merges main into help50 branch
dmalan
marked this pull request as ready for review
October 5, 2024 23:51
This comment was marked as off-topic.
This comment was marked as off-topic.
Contributor
Thanks for the reminder. How did you perform the test? |
- cli.sh: only start help50 in interactive shells with a terminal, else non-interactive login shells (bash --login -c) hang on script - lib/cli: _ansi and _fold take arguments by $#, not -t 0, so messages aren't dropped when stdin is redirected; _fold falls back to 80 columns - valgrind: source lib and use _alert/_ansi instead of undefined _help - help50.sh: fix _rhetocial typo; cap _helpless payload at 8 KiB - help50/python: handle python dir/file.py, use realpath --canonicalize-missing - Dockerfile: install bsdextrautils explicitly for col
tests/smoke.sh checks a built image under timeouts: non-interactive login shells exit, help50 deps are installed, wrappers print with stdin redirected. Run via make smoke, and in CI before pushing to Docker Hub.
Fix help50 bugs and add smoke test
This was referenced Sep 21, 2026
script records everything on the pty, including tab-completion listings, history recall, and redrawn prompts. _help50 dropped only the first line as the command, so after tab-completing a command with no output, the leftover echo was treated as its output and passed to _helpless. Now drop everything through the first line that ends with the command as recorded in history, joining backslash continuations and their PS2 prompts; fall back to dropping the first logical line if the command isn't found.
Ignore terminal echo before the command line in typescripts
The typescript was capped with head -n 1024, keeping the start of the output. Errors are usually at the end (tracebacks, make: *** Error, segfaults), so a program that printed a lot and then failed lost its error before any helper or _helpless saw it. Now keep the first 64 lines (where the command line is echoed and found) plus the last 1024, with a marker for what was omitted. _helpless now also receives the command line as a second argument, so whatever explains the output (in cs50/codespace, the CS50 Duck via cs50.ai) can see what was run. The default _helpless ignores it; output stays the first argument, so the codespace's empty-output check is unaffected.
Nothing exercised _help50 itself: the head/tail cap and the new command-line argument were only checked by hand under a pty. Drive _help50 directly in the image instead, with a fabricated typescript and a fake _helpless, and assert that a command printing 3000 lines then an error yields the command line as the second argument and, as the first, output that starts at the program's first line, carries the omission marker, drops the middle, and ends with the error. Also check that a failed command with no output yields an empty first argument, which is what the codespace's empty-output check depends on. The check fails against the previous help50.sh.
Keep the end of long output, and pass the command line to _helpless
Students used to run `help50 make foo`. Help now arrives automatically after any failed command, so run COMMAND with the same exit status and let the prompt hook handle the rest: help50 make foo behaves exactly like make foo. Builtins go through bash -c with the shell's error wording preserved; an unknown command fails with the shell's own "command not found" message so helpers match it. Bare help50 prints usage (exit 0) explaining the new behaviour. The subcommands (start/stop/...) are unchanged; only they require non-root.
Within a help50 session, define help50 as a shell function that evals COMMAND in the calling shell, so aliases (rm -i), functions, and cd behave exactly as they would directly, and no stderr is rewritten through an unwaited sed. In the script (outside a session), route path-shaped names (./foo.c, ./dir) through bash -c so the shell's own Permission denied / Is a directory errors reach the helpers instead of a synthesized "command not found"; wait for sed before exiting; pass -- to type so option-like commands don't leak usage noise. In the prompt hook, treat `help50 COMMAND` as COMMAND when deriving argv, so helpers that look at positional words (e.g., `check 50`) and the ./ re-make hint still fire. Smoke-test the passthrough: exit statuses, exact error text for unknown, option-like, builtin, and path-shaped commands, usage, sudo, the in-shell function, and the hook's argv handling.
Run `help50 COMMAND` as though COMMAND were typed directly
- 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.
Bound the prompt hook's cost, and add a kill switch
rongxin-liu
approved these changes
Sep 30, 2026
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.
To be squashed when merging.
This new version is implemented in Bash (instead of Python) as follows, wherein usage is inspired by
systemctl, even though it doesn't run as a daemon but, rather, per login shell. It runs locally and automatically now, without any server. In a codespace, failures that no local helper recognizes can additionally be handed to the CS50 Duck (see Related repositories below); in plaincs50/cli, help is printed in the terminal.help50 startsets$HELP50to/tmp/help50.$PPID(the PID of the shell in which the command was run) and launchesscript, which logs standard I/O to that file. Started automatically by/etc/profile.d/cli.shfor interactive shells that have a terminal, if enabled; never for non-interactive shells likebash --login -c, wherescriptwould otherwise block.help50 stopsendsSIGTERMtoscript, which returns the user to the outer, unhelped shell.help50 statuschecks for$HELP50, which is set only whenhelp50is started for a shell.help50 disablewrites/tmp/help50.lock.help50 enabledeletes/tmp/help50.lock.help50 is-enabledchecks for/tmp/help50.lock, and for$HELP50_DISABLEDin the environment. The latter is a kill switch: any value other than0,false,no, oroffdisables help50 at login and is reported as such, so that, delivered as a Codespaces secret, help50 can be turned off fleet-wide at each student's next codespace start without rebuilding an image.help50 COMMAND [ARGS...]runsCOMMANDas though it had been typed directly, with the same exit status, so that students who remember the oldhelp50 make fooget exactly the behavior ofmake foo. Within a help50 session this is a shell function thatevalsCOMMANDin the shell itself, so aliases, functions, andcdbehave as they would have; outside one,/opt/cs50/bin/help50execs it. Barehelp50prints usage explaining that help is now automatic./etc/profile.d/help50.shis a config that that's only sourced when$HELP50is set.$PROMPT_COMMANDto_help50, which is a Bash function implemented therein that, if the most recent command exited with non-0 status (per$?, ignoring ctl-c and ctl-z), reads$HELP50, strips ANSI and control characters (viaansi2txtandcol), and drops everything through the command line itself as echoed by the terminal (so that tab-completion listings, history recall, and redrawn prompts aren't mistaken for output). The read is bounded to the first 64K and last 1M of the file, and long output is capped to its first 64 and last 1,024 lines with a marker in between, so the prompt's cost doesn't grow with how much a program printed before failing. What remains is passed as standard input, with the command's own words as arguments, to each executable in/opt/cs50/lib/help50/(implemented in any language), each under a 5-secondtimeout._helpfulfunction that, by default, displays it in yellow to help the user (and, for one prompt, aliasesy,yes,n, andnoto a reminder that the question was rhetorical, sinceyesandnare real programs)._helpless OUTPUT CMDinstead, which doesn't do anything incs50/clibut is overridden incs50/codespaceto relay them to ddb50._helpedis called, which doesn't do anything incs50/clibut is overridden incs50/codespaceto indicate to the user that help is (no longer) available./etc/profile.d/cli.shstartshelp50automatically, as above./opt/cs50/lib/clicontains several helper functions (written in Bash) that our own wrappers andhelp50use:_alert,_ansi,_find,_fold,_sure. Themake,sqlite3,http-server, andvalgrindwrappers now use them, and themakewrapper letsmakeitself reportmake foo.cso that themakehelper can explain it./opt/cs50/lib/help50/contains helpers forbash(e.g.,1s, a capitalized command,check 50,./foo.c,.\foo, a directory run as a command, code typed into the terminal),cd(e.g.,cd.., or a directory that exists elsewhere in the workspace),clang(a file withoutmain),make(e.g.,make foo.c, or a target whose.cfile is elsewhere), andpython(e.g., a file shadowingcs50,re, orstring, or a file that exists elsewhere). Each verifies its hypothesis against the filesystem before advising; "elsewhere" searches$WORKDIR.Dockerfileinstallsbsdextrautils(forcol),colorized-logs(foransi2txt),file,expect, andfzf, and no longer installs the Pythonhelp50package.tests/smoke.sh(alsomake smoke) checks a built image under timeouts, and.github/workflows/main.ymlruns it against each architecture's build before pushing to Docker Hub.(In a codespace,
scriptspawnsbash -crather thansh -c, per$SHELL, and the outermost shell is VS Code'sbash --login -i.)Related repositories
This PR is the foundation; the rest of the pipeline lives elsewhere. Everything below is merged to its integration branch unless noted.
help50controller (this PR)_helpless, #247help50 COMMANDpassthrough, #248 bounded read, helper timeout,HELP50_DISABLED_helpful/_helpless/_helpedto relay to the help50 extension viacommand50; setsWORKDIRto the workspace; installshelp50.vsix; Sysadmins terminal profile; smoke testcanary); #200 rollout tomainrequestGptResponseposts the transcript to cs50.ai's/api/v1/helpcommand50, the shell-to-VS Code bridge (WebSocket) that the codespace overrides useHELP50_DISABLEDkill switch as a per-student Codespaces secret when set on the server/api/v1/help(added 2024-10-09) wraps the transcript in thehelp50_*prompts ofconfigs/chat_cs50.ymlhelp50 make helloadvice replaced with what students now see2026/fall), #486 (2026/x)Data flow in a codespace: failed command →
_help50→ helpers →_helpless OUTPUT CMD→command50 help50.showButton ask "$ CMD\nOUTPUT"→ help50.vsix shows the button → click → ddb50requestGptResponse→POST https://cs50.ai/api/v1/help.Progression
/api/v1/helpand help50.vsix were created alongside (2024-10).mainintohelp50.canary(Add help50 integration codespace#196–Adds support for R, tidied Dockerfile #198) with help50.vsix#1–adds stdeb and its dependencies #2, ddb50.vsix#23–added server as reverse dependency #24, and cs50.vsix#58–installing gems in GEM_HOME #59, and was verified end to end in a canary codespace.help50 COMMANDpassthrough (Runhelp50 COMMANDas though COMMAND were typed directly #247).HELP50_DISABLED(Bound the prompt hook's cost, and add a kill switch #248); cold-start fixes in ddb50.vsix#25 and help50.vsix#3; kill switch delivery via cs50.dev#212. Codespace changes promoted tomain(Help50 rollout codespace#200). Documentation: cs50.readthedocs.io#184, and course pages in cs50/harvard#485 and #486.Rollout
cs50/cli:latest.cs50/codespace(main, already merged via Help50 rollout codespace#200), which builds on it.Escape hatches, should help50 misbehave: ctl-c interrupts the prompt hook;
help50 stopfor the current shell;help50 disablefor a codespace's new shells; a student's ownHELP50_DISABLEDCodespaces secret; and, fleet-wide without a rebuild,HELP50_DISABLED=1on cs50.dev (0deletes the secret again). Root shells (the codespace's Sysadmins profile) never start help50.