Fix dangling reference in scanner event proxy and missing transaction in share artwork

- main.cpp: proxyScannerEventsToApplication() captured its local `postAll`
  lambda by reference ([&]) inside connections registered on the scanner
  service's long-lived signals. Those connections outlive the function call,
  so every scan event since startup was invoking a dangling reference to an
  already-popped stack frame - undefined behavior, deep in Wt's own signal
  dispatch, from the very first scan onward. This lines up with what we're
  seeing in production: sporadic SIGSEGV crashes (exit code 139) inside
  Wt::Signals::Impl::SignalLinkBase::isConnected(), confirmed via host dmesg,
  roughly every 1-2 hours of uptime. Fixed by capturing `postAll` by value
  (it's a stateless lambda, so this is free); `server` remains a valid
  reference since it lives for the whole program.

- PublicShareResource.cpp: the /artwork branch called
  IArtworkService::getImage(), which lazily resolves artwork through the DB,
  outside of any transaction (the read transaction that populated `items`
  had already closed). Confirmed in production logs: "Uncaught exception
  from handleRequest (aborting request): Operation requires an active
  transaction". Wrapped it in a read transaction, matching the pattern used
  elsewhere in this file.
This commit is contained in:
2026-08-20 10:56:50 -05:00
parent 6d0f8fcc5a
commit 684952a710
2 changed files with 16 additions and 5 deletions
+5
View File
@@ -186,6 +186,11 @@ namespace lms
if (parts.size() != 3) { response.setStatus(404); return; }
const auto item{ std::find_if(items.begin(), items.end(), [&](const auto& value) { return value.id == parts[2]; }) };
if (item == items.end()) { response.setStatus(404); return; }
// IArtworkService::getImage() resolves the artwork through the DB (Wt::Dbo
// lazy-loads on access), so this needs an active transaction on this thread's
// session, same as the block above that populated `items`. Without it, this
// throws "Operation requires an active transaction" (seen in production logs).
auto transaction = _db.getTLSSession().createReadTransaction();
auto artworkService{ core::Service<artwork::IArtworkService>::get() };
auto image{ item->artworkId.isValid() ? artworkService->getImage(item->artworkId, 256) : nullptr };
if (!image) image = artworkService->getDefaultReleaseArtwork();
+11 -5
View File
@@ -241,6 +241,12 @@ namespace lms
void proxyScannerEventsToApplication(scanner::IScannerService& scanner, Wt::WServer& server)
{
// NOTE: these connections are stored on `scanner`, which outlives this function call
// (it's a core::Service owned by main()'s scope), so nothing here may capture a
// reference to a local of this function. `postAll` used to be captured via `[&]`,
// which left every connected slot holding a dangling reference to this function's
// (long-since-popped) stack frame for the remainder of the program's lifetime.
// Capture it by value instead; it's a stateless lambda, so this is free.
auto postAll{ [](Wt::WServer& server, std::function<void()> cb) {
server.postAll([cb = std::move(cb)] {
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
@@ -249,35 +255,35 @@ namespace lms
});
} };
scanner.getEvents().scanAborted.connect([&] {
scanner.getEvents().scanAborted.connect([postAll, &server] {
postAll(server, [] {
LmsApp->getScannerEvents().scanAborted.emit();
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanStarted.connect([&] {
scanner.getEvents().scanStarted.connect([postAll, &server] {
postAll(server, [] {
LmsApp->getScannerEvents().scanStarted.emit();
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanComplete.connect([&](const scanner::ScanStats& stats) {
scanner.getEvents().scanComplete.connect([postAll, &server](const scanner::ScanStats& stats) {
postAll(server, [=] {
LmsApp->getScannerEvents().scanComplete.emit(stats);
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanInProgress.connect([&](const scanner::ScanStepStats& stats) {
scanner.getEvents().scanInProgress.connect([postAll, &server](const scanner::ScanStepStats& stats) {
postAll(server, [=] {
LmsApp->getScannerEvents().scanInProgress.emit(stats);
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime) {
scanner.getEvents().scanScheduled.connect([postAll, &server](const Wt::WDateTime dateTime) {
postAll(server, [=] {
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
LmsApp->triggerUpdate();