feat: a crash window that shows the crash report and sends it only on Send Help #141
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!141
Loading…
Reference in a new issue
No description provided.
Delete branch "gab9281/Longshot:feat/crash-report-window"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
When Longshot crashes, the player gets nothing and the team learns nothing unless someone digs out
longshot_log.txtand posts it. With this PR and LongshotLauncher#46, a window opens after a crash. It shows exactly what a report holds, andsends it to #81 only when the player presses Send Help. I am sorry sends nothing.
Since the review, the window is the Longshot Launcher's (
--crash <folder>, LongshotLauncher#46): main removed both builder windows (#171, #181). This PR keeps the report (nx2/crash_report.py), the game's side (host/hos_crash_window.c, which startsbuilder=, the launcher) and thereport:lines.What changed
docs/crash-report-window.md: what the player sees, what a report holds and never holds, the release flag,how the parts fit, Always send, the steps left, and what testing on real builds found. The beans moved to the launcher.
nx2/crash_report.py, run by the launcher's crash window and imported by the tests:pending()finds the crashed run's log throughcrash_pending.txt, including after a rotation.build()makes the report:CRASH:lines without the register and memory rows, and the 150 log lines before them.~(Wine and Proton'sZ:\home\<name>too), and the user name is redacted wherever it appears as a word (e.g.-home-<user>-in apath written with dashes), in any case, except a name that is also a word of the report's lines (a Steam Deck's
user is
deck), in its own case only, so the report still says "Steam Deck". Every field stays within the relay's limits. The OS and CPU come from the machine when the log has no line.libc.so.6+0x…without the library's path, andunknown+0x10for aplace in no module.
send()posts it to the relay. A 2xx is sent whatever its body; anything that goes wrong before an answer keepsthe report for next time.
nx2/addresses.ini, not in the code (one place to edit when a service moves).card(),settings_text(),log_text(): the window's rows and Show texts.--window(one line of JSON: the rows, the Show texts, the marker's time; nothing sent) and--send --json --time <t>(the send's answer; the marker dropped unless the report is kept). The send is a processof its own: the window can close during a slow send and it finishes.
clear(folder, when)drops only the marker of the crash a window opened for.always_send()andset_always_send()are the launcher's tick.comment_link()is the Sent page's link.host/hos_crash_window.c:hos_crash_window_init()builds the command once at startup, fromlongshot_version.txt'sbuilder=(thelauncher that built the folder:
LONGSHOT_BUILDER_EXE) andbuilder_args=.hos_crash_window_open()runs inreport_crash(host/mkw_glue.c) after the log is flushed. It writescrash_pending.txt, then starts<launcher> --crash <folder>.CreateProcessW, detached.posix_spawnfrom the signal handler, its attributes made at startup: every signal at its default andnone blocked, its own session, output to
/dev/null, every fd above 2 closed.LongshotBuilder.exeanswer--crashwith nothing (a folder they built names them asbuilder=).LONGSHOT_NO_CRASH_WINDOWfollows the switches' rule (env_on: 0, false, off, no are off).report: gpu|cpu|os|rendererlines in the startup log, which the report reads:cpu_text()andos_text()ingpu/vk/vk_bug_report.cpp;gpu/vk/vk_main.cppandhost/rx_backend.c.tests/test_crash_report.py: 31 tests on the report,--windowand--send --jsonamong them.tests/test_crash_window.py: compileshos_crash_window.cwith a stand-in launcher; the window starts with thecrash's signal unblocked and none of the game's files.
Flags and settings
crash_reports(longshot_options.ini)0: a crash opens no window and writes no markerLONGSHOT_NO_CRASH_WINDOW=1longshot_crash.inialways_send=1--window,--send --json [--time <t>](nx2/crash_report.py)Measurements
The new code runs at startup (it builds one command) and at a crash, so the check is that startup and play are
unchanged, and how long the window takes after a crash. Measured with
RX_VK_FRAME_LOG, the game started as thelauncher starts it, 60 s each, no input (the title and the attract demo), frames from 20 s on; interleaved main, PR,
main, PR in the same folder layout (
work/game-linux-mainandwork/game-linux, built the same way). RTX 3060 Laptop(NVIDIA 620.70.06), Ryzen 7 6800H, 15 GB, CachyOS (Linux 7.3.3), KDE Wayland, the default settings.
main:
6b7fa356(2026-11-13). The PR is now on main @514591a3; the numbers still hold: since bb847be7 the game'scode changed only in how the crash starts the window (
posix_spawn, its attributes made once at startup), whichplay never runs. The crash row was measured again at
8cdc358d(below).this PR: feat/crash-report-window @ bb847be7 (the game's code is the same at
6c9b7e7a: after it came the builders'and the report's review fixes, and a rebase on main @
36a12f6f: the website, the compile-shaders choice, the builders'self-update removal; none of it in the game's start or play measured here)
6b7fa356kill -SEGVat 25 s) to<launcher> --crashrunning (s), median (runs)8cdc358dwith the launcher: 0.11Startup and play are the same within run-to-run noise (the title runs at its 30 fps either way). The crash row is when
the window's process is running; its window is drawn a moment after.
Testing
Tested:
RX_CPM=<the CPM cache> python -I -B tests/run_all.pyon11684c08(main @514591a3): 519 passed, 1 failed,15 skipped (Windows only); the failure,
tests/test_menu_i18n.py(a pt_BR text of a menu line main changed), fails onmain
514591a3too. On25ee9472(main @fbd42024): 516 passed, 0 failed.tests/test_crash_window.py's two new checks fail without the child's fix and pass withit.
hos_crash_window.cbuilds with llvm-mingw-Werror.8cdc358d(and again from25ee9472, the posix_spawn start: the same results,SigIgnonly the launcher's own SIGPIPE) into a folder whosebuilder=is the launcher (set by hand: a build from a checkout doesn't write the stamp), on an RTX 3060 Laptop (NVIDIA 620.70.06), a Ryzen 7 6800H with 15 GB, CachyOS (Linux7.3.3), KDE Wayland. Started as the launcher starts it,
kill -SEGVat 25 s:--crashran 0.11 s later, withSigBlk: 0000000000000000(the crash's signal unblocked) and noneof the game's files open;
libc.so.6+0xb5a22, 150 log lines;sent, and the marker dropped.
crash on Linux and on Windows, and the relay's parser (Bean-Ops) accepted both reports.
Not tested:
address (Windows, before the move), both before the first map. So the map is "Title and intro".
source=yet (a step left), so every report is "not a release"for now.
Needs other testers:
Help posts to #81, expecting the window within a few seconds and the comment's link working (T-YFML; its notes
will be updated for the launcher).
report: gpuline names the card and driver, expecting both in the window's Graphics cardrow (T-FTCD).
Screenshots
main @
514591a3has no crash window: a crash ends the game with nothing on screen. This PR @8cdc358dwithLongshotLauncher#46 @ 86f79d2, a real crash (
kill -SEGVon the running game), nothing edited:The builders' windows of the earlier rounds (Linux Tk, Windows Qt under Wine) are gone with the builders.
Risk and rollback
hos_crash_window_open()runs inside the crash handler. On Linux that's a signalhandler, so it uses only
open/write/vfork/execveand allocates nothing. If it fails, the crash ends as itdoes on
main, with the marker left for the launcher's next start.crash_reports=0orLONGSHOT_NO_CRASH_WINDOW=1. Reverting the PR is enough otherwise.~,and settings like bindings and the microphone's name are never sent.
Reviewer notes
host/hos_crash_window.c(what runs in the crash handler:posix_spawnwith attributes made at startup), thennx2/crash_report.pybuild()(whatleaves the PC). The window is LongshotLauncher#46; both should merge together (the launcher's window says "This
build's tools can't read crash reports yet" with builders from before this PR).
longshot_version.txt, so nobuilder=line and no window. Releases haveone.
longshot_log.txtonly when it isn't started from a terminal. Started from a terminal, it logsthere, and the window finds no crash to report.
source=line (the release flag);longshot_defaults.ini;recomplauncher.json;releasefield,[not a release]) is already deployed in Bean-Ops.Issues
See #81
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.hos_crash_window.cagain with llvm-mingw-Werror; therx_backend.cchange is one log line.)🤖 Generated with Claude Code
https://claude.ai/code/session_015KgSKoUqUvce5UXG89NBgi
1f0e9dc5f5edfc1ef3e1edfc1ef3e1468fe236df468fe236df752ff88994752ff88994715cf050dafeat/crash-report-windowto WIP: feat: a crash window that shows the crash report and sends it only on Send HelpUpdated to 715cf050, rebased on main @
39647e1a(a CI-only change).assets/magicbean-report-sent.png, Magic Bean's own art, which replaces the earlierbean; the license box is ticked.
own comment on #81.
python -I -B tests/run_all.py: 477 passed, 0 failed, 64 skipped.🤖 Generated with Claude Code
WIP: feat: a crash window that shows the crash report and sends it only on Send Helpto feat: a crash window that shows the crash report and sends it only on Send Help715cf050dabb847be7c1Updated to bb847be7, rebased on main @
6b7fa356(#148)./code-review(high) of the whole PR found 10 things, all fixed:send()keeps the report when the answer can't be read.LONGSHOT_NO_CRASH_WINDOWfollowsenv_on.deck).--launchwith a crash waiting goes on to the launcher page.__init__.nx2/crash_report.py.nx2/addresses.ini.run_all: 522 passed, 0 failed.testing wantedlabel before anyone can claim them.Rebased on main @
36a12f6f(#161 removed the builders' self-update: the crash window's lines kept on top of it, in the Linux builder's docstring, Builder() and the launcher page, and the Qt start's docstring). run_all: 520 passed, 1 failed (test_prepare_cache_save, which fails the same on main @36a12f6f). The Measurements' main and PR lines added.bb847be7c177609f451d- send(): a 2xx is sent, whatever its body: never kept, never sent again (a report the relay posted was offered again before). - Linux window: closing it with the title bar's X, started from the launcher or Play, goes on to them (as Qt's closeEvent); a Send Help pressed before the always-send tick's cancels that one, and a refused report's disabled Send isn't sent by Enter; the Sent page from the launcher has no Play again (Close goes back to it, as on Qt). - nx2/addresses.ini: a BOM or a file that doesn't parse turns the uses off instead of stopping the builders. - A microphone picked earlier in the session never reaches the log lines ("audio in: <name> open/is back/..."). - The user name is matched in any case again, except a name that is also a word of the report's lines (deck, arch, ...), in its own case. - this_cpu() and this_os() word the line as the game does (the RAM, the desktop and session, the AppImage, Wine). - The windows say where reports go, and the Sent link's number, from crash_issue; without it, no place and no link. - Qt pending(): no tools lookup when there is no folder. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015KgSKoUqUvce5UXG89NBgiUpdated to
6c9b7e7a: a second /code-review (high) of the follow-ups found 10 things. 8 fixed: a 2xx is sent whatever its body (no duplicate on #81); the Linux window's X goes on to the launcher or Play as on Qt; the always-send timer is cancelled by a pressed Send Help, and a refused report isn't sent by Enter; no Play again on the Sent page from the launcher; addresses.ini with a BOM or broken never stops the builders; earlier microphones redacted from 'audio in:' lines; the user name in any case again except words of the report's lines (deck, arch); this_cpu/this_os word the line as the game does; the windows' issue text from crash_issue. Left: the logs read twice at --launch with a crash waiting (a few small files), and pipeline_share's own #14 address (main's code, not this PR's). run_all: 520 passed, 1 failed (test_prepare_cache_save, as on main).The privacy design holds up. Nothing is sent without Send Help or the opt-in "Always send" tick. The payload is exactly what the window shows. Register and memory rows, bindings, the microphone name and the hardware hashes stay out. The handler does no allocation for its own part. The send runs on a thread, so the window stays responsive. This can't merge as it is, though.
Blocking: the windows no longer exist on main. This branch forks from
36a12f6f. Since then main has removed both windows the crash UI lives in:builder/py(the Qt builder:crash.py,window.py,longshot_builder_start.py).builder/linux/longshot_builder.py(the Tk window) and made the Builder command-line only.On main,
AppRunandLongshotBuilder.exeanswer any other argument with the "get the launcher" message and exit 1. So<builder> --crash <folder>fromhos_crash_window.cwould show that message box instead of a report. The window needs a new home, most likely the Longshot Launcher (or a small standalone window shipped with the release).hos_crash_window.cshould start that. The "Rebased on current main" box is not true now.What carries over as is:
nx2/crash_report.py,tests/test_crash_report.py, thereport:log lines and most of the host side. Two fixes for those parts, inline:on_driver_fault(gpu/vk/vk_draw.cpp) has noSA_NODEFER, and every Linux Vulkan run installs it.execvekeeps the signal mask. Whatever the window starts later, like Play again or the launcher, inherits it too. A game started that way loses its crash handler: the kernel kills it on the next fault, with noCRASH:lines and no report. Clear the mask in the child.Z:\home\<name>\…matches none of the patterns inHOMES, and the user-name pass only knows the Wine prefix'ssteamuser. The PR's own Windows test ran under Wine.Smaller points:
timeout=10limits each socket step, not DNS or the whole send, so a bad network can leave the window impossible to close for longer than 10 s.data.magicbean.gmatte.xyzfirst. Is that the long-term host? It's the place players' IPs and reports land, and the window names only git.magicbean.dev.I didn't build this or run the tests. This review comes from reading the code. I also ran
redact()by itself on sample paths to confirm the Wine gap.@ -0,0 +173,4 @@setsid(); /* (its own session: it outlives this process, and a terminal's Ctrl+C doesn't reach it) */const int null = open("/dev/null", O_RDWR);if (null >= 0) dup2(null, 0), dup2(null, 1), dup2(null, 2);execve(builder, window_argv, environ);The child inherits the crashing thread's signal mask, and
execvekeeps it. On Linux every Vulkan run installson_driver_fault(gpu/vk/vk_draw.cpp,install_driver_fault_guard) withSA_SIGINFObut noSA_NODEFER, so a crash reachesreport_crashwith SIGSEGV (or SIGBUS) blocked. The window then starts with it blocked, and so does anything it starts: Play again's game, or the launcher. A game started that way has its crash handler skipped: a fault in it is delivered with SIG_DFL, so it dies with noCRASH:lines and no report. Clear the mask beforeexecve.sigprocmaskis async-signal-safe:While you're here: the game's fds without
O_CLOEXEC(the log, sockets, the GPU device) also pass to the window, which outlives the game.close_range(3, ~0U, 0)in the child would drop them (with a fallback where it's missing).@ -0,0 +3,4 @@# isn't sent and the window says so (it is kept and offered again).[addresses]# The crash relay (Bean-Ops deploy/crash-relay): POST a crash reportcrash_reports = https://data.magicbean.gmatte.xyz/crashQuestion: is
data.magicbean.gmatte.xyzthe long-term host for the relay? It's where players' reports and IP addresses go first, and the window only names git.magicbean.dev. If it's going to move to a magicbean.dev name, it's better to do it before the first release that sends.@ -0,0 +86,4 @@NOT_SENT = re.compile(r"^(key_|pad_)|^(microphone_device|auto_for|auto_tuned)$")HOMES = [(re.compile(r"/home/[^/\s]+"), "~"), (re.compile(r"/var/home/[^/\s]+"), "~"), (re.compile(r"/Users/[^/\s]+"), "~"),(re.compile(r"[A-Za-z]:[\\/]+Users[\\/]+[^\\/\s]+", re.I), "%USERPROFILE%")]Under Wine or Proton, which is how the PR's Windows test ran, home is the prefix's
C:\users\steamuser. But the game folder is often on the Linux side,Z:\home\<name>\Games\Longshot. None of the patterns match\home\with backslashes, and the user-name pass only knowssteamuser. Soredact(r"Z:\home\gabriel\Games\Longshot\bin", "C:\\Users\\steamuser", "steamuser")returns the path withgabrielstill in it. Add a drive-letterhomepattern (e.g.[A-Za-z]:[\\/]+home[\\/]+[^\\/\s]+→~), and add the case totests/test_crash_report.py.@ -0,0 +448,4 @@req = urllib.request.Request(url, data=json.dumps(report).encode(), method="POST",headers={"Content-Type": "application/json", "User-Agent": "longshot-crash-report"})try:with urllib.request.urlopen(req, timeout=timeout) as resp:Nit:
timeout=10limits each socket operation, not the whole send. DNS isn't covered, and a slow answer resets the timer on each read. Both windows ignore close whilebusy, so on a bad network the player can't close the window for longer than 10 s. Since the send thread is a daemon, closing while busy could just keep the report (like a failed send) instead of being refused.0d8cc5b5ba8cdc358d1ffeat: a crash window that shows the crash report and sends it only on Send Helpto WIP: feat: a crash window that shows the crash report and sends it only on Send Help8cdc358d1f25ee9472aeThanks for the review. Each point, in the order you raised them:
Blocking: the windows no longer exist on main. The window is now the Longshot Launcher's: LongshotLauncher#46,
--crash <folder>. The game already startsbuilder=, and the launcher writes itself there (LONGSHOT_BUILDER_EXE), sohost/hos_crash_window.cstarts it with no change to how it finds it. This PR is rebased on main (main @fbd42024, at25ee9472). The builder windows, their test and the beans (now in the launcher) are gone. What stays here is what you listed as carrying over:nx2/crash_report.py, its tests, thereport:lines and the host side.The report stays one implementation. The launcher runs this version's
nx2/crash_report.pywith that version's Python:--windowfor what to show (one line of JSON, nothing sent), then--send --json --time <t>after Send Help. The folders that Longshot's own command-line builders make name them asbuilder=, so the AppImage andLongshotBuilder.exenow answer--crashwith nothing. A crash no longer shows their "get the launcher" box; the marker waits for the launcher's next start.1. Signal mask. Fixed, and taken further after a second review: the window now starts with
posix_spawn, its attributes made at startup. Every signal is back to its default and none is blocked, it gets a session of its own, its output goes to/dev/null, and every fd above 2 is closed (addclosefrom_np, whatever the fd limit). The game's handlers can't run in the child either, which they could between unblocking andexecvein the vfork version.tests/test_crash_window.pychecks it from a stand-in launcher:SigBlkwithout SIGSEGV,SigIgnwithout the game's SIGUSR1, and none of the game's files. Those checks fail on the previous code. In the game: SIGSEGV at 25 s had the launcher's--crashrunning 0.11 s later withSigBlk: 0000000000000000and none of the game's files open (at25ee9472, withSigIgnonly the launcher's own SIGPIPE).2. Wine and Proton paths.
Z:\home\<name>andz:/var/home/<name>become~, and your example is now intests/test_crash_report.py. A drive has to be a letter on its own, sofile:///home/<name>keeps itsfile://. While there:/var/home/<name>used to come out as/var~, because/home/matched first. The more specific patterns now go first.Close during a send. The send is a process of its own, so the window closes at once and the send finishes without it. The report is kept unless it was sent or refused. A send is also only of the crash the window showed: if another crash took the marker meanwhile, nothing is sent and that crash gets its own window.
The relay's host.
data.magicbean.gmatte.xyzstays for now. It is only innx2/addresses.ini, so moving it is a one-line change, and the doc's open questions now say to do it before the first release that sends."Rebased on current main" is true again. #141 and LongshotLauncher#46 should merge together.
WIP: feat: a crash window that shows the crash report and sends it only on Send Helpto feat: a crash window that shows the crash report and sends it only on Send Help25ee9472ae11684c08e7View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.