Claude screenshot crashes Hise
-
@Christoph-Hart Seems to happen a lot more since the last commits
CLAUDE:
HISE 4.9.3 (102de5c4) — recurrent SIGABRT via the REST API screenshot endpoints
Symptom. HISE dies with
Abort trap: 6.
8 of the 33 crash reports on this machine (2026‑08‑19 → 2026‑08‑23) have byte‑identical stacks:std::terminate() → juce::MessageQueue::runLoopSourceCallback(void*) → __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ → …i.e. something escapes a MessageManager::callAsync callback. The stack is already unwound, so the report names no culprit — which is why this has been hard to pin down.
Correlation. Every occurrence I could attribute happened during
GET /api/testing/screenshot. Two crashes today were directly on that call; two earlier sessions logged500 — /api/testing/screenshot?moduleId=Interface&outputPath=…shortly before dying. It hits both the cropped (&id=) and the plainmoduleId‑only form.Root cause (
hi_backend/backend/ai_tools/RestHelpers.cpp:2539).SafeAsyncCall::callAsyncIfNotOnMessageThread<...>(*sp, [&](...) // fire-and-forget { hise::ScriptContentComponent component(&spp); capturedImage = component.createComponentSnapshot(cropBounds, true, scale); captureSuccess = capturedImage.isValid(); captureComplete.signal(); }); if (!captureComplete.wait(1000)) return req->fail(500, "screenshot capture timed out"); // ← stack frame dies herecallAsyncIfNotOnMessageThreadisMessageManager::callAsync(MiscToolClasses.h:87) — it does not wait. The lambda capturescapturedImage,captureSuccessandcaptureCompleteby reference off the REST thread's stack. When the message thread takes longer than 1000 ms, the handler returns, that frame is destroyed, and the lambda then writes into freed stack memory and signals a destroyedWaitableEvent. Hence: 500 first, abort shortly after.1000 ms is easy to blow — the lambda constructs a fresh
ScriptContentComponentand paints every script component, including all custom paint routines, on a 1600×900 interface.Same pattern, two more sites:
RestHelpers.cpp:2725(testing/e2e, 30 s) and:6508(dsp/screenshot, 1500 ms — and that lambda spends 500 ms inrunDispatchLoopUntilbefore capturing, re-entering the message loop).Suggested fix.
SafeAsyncCall::callAsyncAndWaitalready exists in the same header (MiscToolClasses.h:104) and is documented to return false on timeout/deletion. Failing that: heap‑allocate the shared state in aReferenceCountedObject/shared_ptrcaptured by value, so a timed‑out request cannot outlive it. Atry/catcharound the lambda body would also convert the abort into a 500.
Two caveats so you can present it accurately: the other 5 SIGABRTs I can't attribute to the screenshot endpoint from the crash file alone — the unwound stack gives nothing. And the dangling-frame mechanism is provable from the code and matches the 500-then-crash sequence, but I couldn't confirm which exception actually terminates, only that one escapes a message-thread callback.
-
@ustk what's a Claude screenshot?
-
@David-Healey When Claude makes modifications on UI, it makes a screenshot using the interaction test window to see if it renders as it should
-
@ustk Same here, but I don't do UI interaction loops with Claude much so it hasn't affected me.
Post a bug on GitHub.
My Claude says:
+1, and confirmed on current develop (ca472e27b, 2026-08-19): the pattern ustk describes is still there at all three sites. For what it's worth the code is from February (30c732d9b, "added Rest API endpoint /screenshot"), so I don't think the recent commits introduced it, they just make the message thread busier so the 1 s timeout trips more often.
One note on the suggested fixes: switching to
SafeAsyncCall::callAsyncAndWaiton its own isn't enough. It heap-allocates theWaitableEvent(so thesignal()on a dead event goes away), but the lambda still capturescapturedImageandcaptureSuccessby reference, so a late-running lambda still writes into the handler's dead stack frame. The state the lambda writes to has to outlive the handler too. Ashared_ptrto a small result struct, captured by value, does it:struct ScreenshotCapture { Image image; bool success = false; WaitableEvent done; }; auto capture = std::make_shared<ScreenshotCapture>(); auto sp = dynamic_cast<ProcessorWithScriptingContent*>(jp); SafeAsyncCall::callAsyncIfNotOnMessageThread<ProcessorWithScriptingContent>(*sp, [capture, fullBounds, cropBounds, scale](ProcessorWithScriptingContent& spp) { hise::ScriptContentComponent component(&spp); component.setBounds(fullBounds); capture->image = component.createComponentSnapshot(cropBounds, true, scale); capture->success = capture->image.isValid(); capture->done.signal(); }); if (!capture->done.wait(1000)) return req->fail(500, "screenshot capture timed out"); if (!capture->success) return req->fail(500, "failed to capture screenshot"); auto capturedImage = capture->image;If the wait times out, the lambda still runs later and writes into the heap struct, which is kept alive by its own copy of the
shared_ptr. The handler has already returned its 500 and nothing points at the stack any more. (If the processor was deleted in the meantime,SafeAsyncCalleralready skips the call via theWeakReference, so the event is never signalled, which is fine because the waiter has already given up.)Same treatment for the other two:
handleTestingInteraction(~line 2725):testResult+ executed + completed into a shared struct, capture by value./api/dsp/screenshot(~line 6508): same as the screenshot site. Note this one also callsmm->runDispatchLoopUntil(500)inside the lambda, so it spends a third of its 1500 ms budget by design; might be worth bumping that timeout while you're in there.