From e24f996d4d64d6991de32c35b60da00afae89f04 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 12 Oct 2026 09:29:33 +0700 Subject: [PATCH 1/2] fix: keep the Flatpak USB permission request open until it is answered The runtime closed the USB portal request after two minutes and then had no device access until it was restarted. After boot the dialog is easily missed: the runtime has no window, so the dialog is not attached to the app and can open behind it. A user who found it later could not allow access any more (#37). The request now stays open until the user allows or denies access, while the status keeps saying that a request is waiting in the portal. After two minutes the runtime only logs acquire_still_pending once. --- src/portalusb.cpp | 22 +++++++++++----------- src/portalusb.h | 6 ++++-- tests/flatpak/portalusb_tests.cpp | 23 ++++++++++++++--------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/portalusb.cpp b/src/portalusb.cpp index 1033804..554c374 100644 --- a/src/portalusb.cpp +++ b/src/portalusb.cpp @@ -157,10 +157,12 @@ Access::Access(Registry ®istry, QObject *parent) : Access(registry, Options{} Access::Access(Registry ®istry, const Options &options, QObject *parent) : QObject(parent), registry_(registry), options_(options), bus_(QString()) { registerTypes(); - permissionDeadline_.setSingleShot(true); - connect(&permissionDeadline_, &QTimer::timeout, this, [this]() { - logPortalEvent(QStringLiteral("acquire_timeout")); - fail(tr("The USB permission request timed out. Restart the Flatpak background runtime to try again.")); + // A permission dialog that nobody answered after boot used to time out + // and leave the runtime without USB access until it was restarted (#37). + // The dialog now stays open until the user allows or denies access. + pendingNotice_.setSingleShot(true); + connect(&pendingNotice_, &QTimer::timeout, this, [this]() { + logPortalEvent(QStringLiteral("acquire_still_pending")); }); connectionCheck_.setInterval(250); connect(&connectionCheck_, &QTimer::timeout, this, [this]() { @@ -440,7 +442,7 @@ bool Access::maybeAcquire() { // fail() already published the terminal state. return true; } - permissionDeadline_.start(qMax(1, options_.permissionTimeoutMs)); + pendingNotice_.start(qMax(1, options_.pendingNoticeMs)); { QMutexLocker lock(®istry_.mutex_); // A new open request supersedes an earlier denial text. @@ -448,9 +450,7 @@ bool Access::maybeAcquire() { registry_.error_.clear(); } logPortalEvent(QStringLiteral("acquire_requested"), - {{QStringLiteral("product"), printerProductIdString(device.productId)}, - {QStringLiteral("timeout_ms"), - QString::number(qMax(1, options_.permissionTimeoutMs))}}); + {{QStringLiteral("product"), printerProductIdString(device.productId)}}); const QString request = requestPath_; call(owner_, kDesktopPath, kUsbInterface, QStringLiteral("AcquireDevices"), {QString(), QVariant::fromValue(Devices{{device.id, {{QStringLiteral("writable"), true}}}}), @@ -499,7 +499,7 @@ void Access::processAcquisitionResponse(uint response) { logPortalEvent(QStringLiteral("acquire_response"), {{QStringLiteral("code"), QString::number(response)}}); if (response != 0) { - permissionDeadline_.stop(); + pendingNotice_.stop(); requestPath_.clear(); pendingId_.clear(); pendingEndpoint_.clear(); @@ -579,7 +579,7 @@ void Access::finishAcquire() { : QStringLiteral("acquire_discarded")); if (!accepted) releaseDevices({pendingId_}); - permissionDeadline_.stop(); + pendingNotice_.stop(); requestPath_.clear(); pendingId_.clear(); pendingEndpoint_.clear(); @@ -641,7 +641,7 @@ void Access::stop() { running_ = false; ++generation_; sessionReady_ = false; - permissionDeadline_.stop(); + pendingNotice_.stop(); connectionCheck_.stop(); closeRemote(requestPath_, kRequestInterface); closeRemote(sessionPath_, kSessionInterface); diff --git a/src/portalusb.h b/src/portalusb.h index b1d9859..038d4e1 100644 --- a/src/portalusb.h +++ b/src/portalusb.h @@ -87,7 +87,9 @@ class Access final : public QObject { struct Options { QString service = QStringLiteral("org.freedesktop.portal.Desktop"); int callTimeoutMs = 2000; - int permissionTimeoutMs = 120000; + // The permission request stays open until the user answers it; after + // this long the runtime only logs once that it is still waiting. + int pendingNoticeMs = 120000; }; explicit Access(Registry ®istry, QObject *parent = nullptr); Access(Registry ®istry, const Options &options, QObject *parent = nullptr); @@ -128,7 +130,7 @@ private slots: QString connectionName_; QDBusConnection bus_; QDBusServiceWatcher *watcher_ = nullptr; - QTimer permissionDeadline_; + QTimer pendingNotice_; QTimer connectionCheck_; QString owner_; QString sessionPath_; diff --git a/tests/flatpak/portalusb_tests.cpp b/tests/flatpak/portalusb_tests.cpp index ec8b9f2..a2ec985 100644 --- a/tests/flatpak/portalusb_tests.cpp +++ b/tests/flatpak/portalusb_tests.cpp @@ -311,23 +311,28 @@ private slots: QVERIFY(!registry.snapshot().monitoring); } - void permissionTimeoutClosesRequestAndIgnoresLateSuccess() { + // An unanswered permission dialog stays open: a user who only allows + // access later, for example after login, still gets the device (#37). + void unansweredPermissionRequestStaysOpenUntilAllowed() { FakePortal portal; portal.replyAutomatically = false; QVERIFY(portal.install()); Registry registry; Access::Options options; - options.permissionTimeoutMs = 100; + options.pendingNoticeMs = 50; Access access(registry, options); QVERIFY(access.start()); QTRY_COMPARE(portal.acquisitions, 1); - QTRY_VERIFY(!registry.snapshot().error.isEmpty()); - QTRY_VERIFY(portal.request.closed); - QVERIFY(!registry.snapshot().acquisitionPending); + QTest::qWait(200); + QVERIFY(!portal.request.closed); + QVERIFY(registry.snapshot().acquisitionPending); + QVERIFY(registry.snapshot().error.isEmpty()); + QVERIFY(registry.snapshot().monitoring); emit portal.request.Response(0, {}); - QTest::qWait(25); - QCOMPARE(portal.finishes, 0); - QVERIFY(!registry.snapshot().monitoring); + QTRY_VERIFY(registry.snapshot().devices.size() == 1 && + registry.snapshot().devices.first().granted); + QVERIFY(!registry.snapshot().acquisitionPending); + QCOMPARE(portal.acquisitions, 1); } void portalOwnerLossRevokesWithoutReacquiring() { @@ -511,7 +516,7 @@ private slots: "product=\"391a:1021\" readable=\"true\" writable=\"true\" " "same_identity=\"false\" granted=\"false\"$"), QStringLiteral("^tryx_usb_portal event=\"acquire_requested\" " - "product=\"391a:1021\" timeout_ms=\"[0-9]+\"$"), + "product=\"391a:1021\"$"), QStringLiteral("^tryx_usb_portal event=\"acquire_acknowledged\"$"), QStringLiteral("^tryx_usb_portal event=\"acquire_response\" code=\"0\"$"), QStringLiteral("^tryx_usb_portal event=\"acquire_granted\"$"), From 8182389538a05b5914bb588ad42262cec205d8ce Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 19 Oct 2026 10:15:47 +0700 Subject: [PATCH 2/2] docs: list the Loop and Shuffle display stall on Panorama SE firmware v2.0.6 With a carousel of still images the display stops answering after half an hour to several hours, and only a power-off recovers it; one still image ran for more than 10 hours on the same setup (#37). --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 6687b2b..af266f1 100644 --- a/README.md +++ b/README.md @@ -762,6 +762,20 @@ Add it to the kernel command line of your boot loader, reboot, and switch the power supply off once so the display recovers. Details and measurements are in issue #28. +### Display stops answering during Loop or Shuffle playback + +With Loop or Shuffle, the runtime hands the playlist to the display, and the +firmware switches media on its own. On a Panorama SE `391a:1021` with firmware +v2.0.6 and a carousel of still images, the display stopped answering anywhere +from half an hour to several hours into a session. It stays on USB but no +longer accepts data, and the runtime reports that the keepalive stopped after +3 retries. Restarting the runtime or rebinding the USB device does not help; +only switching the power supply off does. One still image ran for more than +10 hours on the same setup. + +Until TRYX fixes the firmware, use Single playback with one image or video. +Details are in issue #37. + ### The display session never starts after the first media upload Before 2.5.3 the runtime created its directories with permissions from the