Skip to content

Scantailor Win-64-Qt6 crashes when recreating new project #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
noobie-iv opened this issue Jan 6, 2025 · 19 comments · Fixed by #52
Closed

Scantailor Win-64-Qt6 crashes when recreating new project #51

noobie-iv opened this issue Jan 6, 2025 · 19 comments · Fixed by #52
Labels
bug Something isn't working complete Performed

Comments

@noobie-iv
Copy link
Member

  1. Open project (Or create new)
  2. Close project
  3. Open project (Or create new)
  4. Close project
  5. Crash

CRASH:
scantailor-experimental-1.2025.01.06-Win32-X86-64-Qt6

OK:
scantailor-experimental-1.2025.01.06-Win32-X86-64
scantailor-experimental-1.2025.01.06-Win32-X86-32

@zvezdochiot zvezdochiot added the bug Something isn't working label Jan 6, 2025
@plzombie
Copy link
Contributor

plzombie commented Jan 6, 2025

Dumps from 1.2024.10.28. This is the version where I moved from Qt 6.7.3 to 6.8.1

@plzombie
Copy link
Contributor

plzombie commented Jan 6, 2025

Короче, я перезалил x64 Qt6 сборку с версией 6.7.3. Под ARM у меня и так 6.7.3 был, так как начиная с 6.8 у меня Qt перестал собираться под ARM (Visual Studio выжирала оперативку или вылетала с неизвестной ошибкой)

@plzombie
Copy link
Contributor

plzombie commented Jan 6, 2025

@zvezdochiot посмотри файлики перевода. Дампит при вызове
OrientationOptionsWidget->setWindowTitle(QCoreApplication::translate("OrientationOptionsWidget", "Form", nullptr));
внутри вызова setupUi в конструкторе
Вот в файликах
изображение

@zvezdochiot
Copy link
Member

zvezdochiot commented Jan 6, 2025

@plzombie . Да в файлах переводов давно все номера строк "поехавшие". В виджетах долгое время производилась перетрубация. Роман как то умеет их в соответствие приводить, а я ни шиша. Сверять все строки вручную? - да это проще застрелиться! Такие вот дела.

@plzombie
Copy link
Contributor

plzombie commented Jan 6, 2025

Я могу поконкретней сказать. Дампит всегда в одном из OptionsWidget, внутри setupUi, внутри retranslateUi.
Даже если переводы выкинуть, всё равно дампит. Копаю дальше...

Ещё конкретней, в MainWindow::switchToNewProject
вызывается конструктор: m_ptrStages.reset(new StageSequence(pages, newPageSelectionAccessor()));

StageSequence::StageSequence(IntrusivePtr<ProjectPages> const& pages,
                             PageSelectionAccessor const& page_selection_accessor)
    :	m_ptrFixOrientationFilter(new fix_orientation::Filter(page_selection_accessor)),
      m_ptrPageSplitFilter(new page_split::Filter(pages, page_selection_accessor)),
      m_ptrDeskewFilter(new deskew::Filter(page_selection_accessor)),
      m_ptrSelectContentFilter(new select_content::Filter(page_selection_accessor)),
      m_ptrPageLayoutFilter(new page_layout::Filter(pages, page_selection_accessor)),
      m_ptrOutputFilter(new output::Filter(page_selection_accessor))

и на одном из new падает уже по схеме выше

@zvezdochiot
Copy link
Member

zvezdochiot commented Jan 6, 2025

@plzombie , а ежели в каждый этап в конструктор происать:

#include <QDebug>
...
    qDebug() << "имя этапа"

на нужный выйти не получится? Они вроде как по порядку создаются.

@plzombie
Copy link
Contributor

plzombie commented Jan 6, 2025

@zvezdochiot Они рандомно падают. Мне кажется, это что-то, связанное с деревом виджетов в Qt. А возможно и тупо баг в 6.8.1, надо найти какой-нибудь дистрибутив линукса, где он есть по умолчанию, и посмотреть

Вот эти вот правками сейчас тестирую, если что:

diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index ecf8764..20ed5fc 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -352,6 +352,7 @@ MainWindow::switchToNewProject(
     updateDisambiguationRecords(pages->toPageSequence(IMAGE_VIEW));

     // Recreate the stages and load their state.
+    m_ptrStages.reset(nullptr);
     m_ptrStages.reset(new StageSequence(pages, newPageSelectionAccessor()));
     if (project_reader)
     {
diff --git a/src/StageSequence.cpp b/src/StageSequence.cpp
index e641ae3..6d6757a 100644
--- a/src/StageSequence.cpp
+++ b/src/StageSequence.cpp
@@ -22,13 +22,20 @@

 StageSequence::StageSequence(IntrusivePtr<ProjectPages> const& pages,
                              PageSelectionAccessor const& page_selection_accessor)
-    :  m_ptrFixOrientationFilter(new fix_orientation::Filter(page_selection_accessor)),
+    /* : m_ptrFixOrientationFilter(new fix_orientation::Filter(page_selection_accessor)),
       m_ptrPageSplitFilter(new page_split::Filter(pages, page_selection_accessor)),
       m_ptrDeskewFilter(new deskew::Filter(page_selection_accessor)),
       m_ptrSelectContentFilter(new select_content::Filter(page_selection_accessor)),
       m_ptrPageLayoutFilter(new page_layout::Filter(pages, page_selection_accessor)),
-      m_ptrOutputFilter(new output::Filter(page_selection_accessor))
+      m_ptrOutputFilter(new output::Filter(page_selection_accessor))*/
 {
+    m_ptrFixOrientationFilter.reset(new fix_orientation::Filter(page_selection_accessor));
+    m_ptrPageSplitFilter.reset(new page_split::Filter(pages, page_selection_accessor));
+    m_ptrDeskewFilter.reset(new deskew::Filter(page_selection_accessor));
+    m_ptrSelectContentFilter.reset(new select_content::Filter(page_selection_accessor));
+    m_ptrPageLayoutFilter.reset(new page_layout::Filter(pages, page_selection_accessor));
+    m_ptrOutputFilter.reset(new output::Filter(page_selection_accessor));
+
     m_fixOrientationFilterIdx = m_filters.size();
     m_filters.push_back(m_ptrFixOrientationFilter);

На Qt 6.7.3 и Qt 5.12.12, понятное дело, ничего не падает.

@plzombie
Copy link
Contributor

plzombie commented Jan 6, 2025

В общем, я не понимаю, как это всё работает. Где и когда этот StageSequence привязывается к окну и где отвязывается. Завтра соберу debug версию Qt 6.8.1, попробую посмотреть, где конкретно падает

@zvezdochiot
Copy link
Member

zvezdochiot commented Jan 6, 2025

@plzombie say:

В общем, я не понимаю, как это всё работает.

У меня случаются падения "на ровном месте". Я даже issue на это заводил. Но они совсем рандомные и логика никак не отслеживается. Думал даже, что у меня либо диск сбоит, либо память. Да и сейчас так думаю, слишком рандомно они происходят.

PS: На этапе "Вывод" (Output) я вывел OutputGenerator из режима const, чтобы получать с него значения метрик. С этого и началась эта рандомная история.

@noobie-iv
Copy link
Member Author

noobie-iv commented Jan 7, 2025

@plzombie @zvezdochiot

Роман как то умеет их в соответствие приводить

lupdate этим занимается. Под виндами у меня скрипт для запуска:

@rem Find lupdate
if exist C:\Prog\Qt\5.12.12\bin\lupdate.exe set LUPDATE=C:\Prog\Qt\5.12.12\bin\lupdate.exe
if exist D:\Prog\Qt\5.12.12\bin\lupdate.exe set LUPDATE=D:\Prog\Qt\5.12.12\bin\lupdate.exe

@rem Save start directory
@set START_DIR=%CD%

@rem Find source directory
@rem Start from VSCode inside source dir
@if exist .\src\version.h @set SOURCE_DIR=%CD%\src
@rem Start from TC/Explorer outside source dir
@if exist .\src\src\version.h @set SOURCE_DIR=%CD%\src\src

@rem Set translations dir
@set TRANSLATION_DIR=%SOURCE_DIR%\translations

@rem go to translation folder
@cd %TRANSLATION_DIR%
@rem Update translation
@for %%f in (scantailor-experimental*.ts) do (
    %LUPDATE% %SOURCE_DIR% -ts %%f
)

@rem Restore start directory
@cd %START_DIR%

Запуск выглядит так:

CD d:\Prog\ScanTailor\Experimental\src\src\translations\
D:\Prog\Qt\5.12.12\bin\lupdate.exe d:\Prog\ScanTailor\Experimental\src\src -ts scantailor-experimental_bg.ts
D:\Prog\Qt\5.12.12\bin\lupdate.exe d:\Prog\ScanTailor\Experimental\src\src -ts scantailor-experimental_cs.ts
D:\Prog\Qt\5.12.12\bin\lupdate.exe d:\Prog\ScanTailor\Experimental\src\src -ts scantailor-experimental_de.ts
...

@noobie-iv
Copy link
Member Author

@plzombie say:

кажется, это что-то, связанное с деревом виджетов

Есть даже самодельный указатель SafeDeletingQObjectPtr. И коммент в исходниках MainWindow:

// We use asynchronous connections because otherwise we
// would be deleting a widget from its event handler, which
// Qt doesn't like.

@plzombie say:

В общем, я не понимаю, как это всё работает.

Я аж MVST затеял, чтобы разобраться. В нем, наверное, треть или четверть базовой логики ST. И я тоже до конца не понимаю, что в ST происходит. А это нужно, чтобы стадию редактирования вставить. Вот трассировка одиночного щелчка мышью при смене стадии в STD (pdf и исходник для draw.io):

Trace.pdf
Trace.draw.io.zip

Трассировка неполная. Там в начале пропущена цепочка CaсheDrivenTask, которая рисует незавершенные эскизы, прежде чем основную обработку запускать. И две цепочки, которые запускают ContentBoxPropagator и PageOrientationPropagator, чтобы принудительно обновить сведения о поворотах и нарезках, чтобы незавершенные эскизы правильно показали повороты и нарезку.

В отладчике будет постоянная скачка между тремя потоками: основной GUI-поток, основная фоновая обработка, фоновая работа кеша эскизов. Фоновые обведены красным на трассировке. Это работают стадии:

  • GUI - поток .Предварительная цепочка CacheDrivenTask - предварительные неполные эскизы (на трассировке не нарисовал, но она есть). Там же где-то два пропагатора, тоже не нарисованы.
  • GUI - поток (UpdateMainArea). Сборка цепочки обработки от начала до текущей стадии.
  • Фоновый поток (WorkerThread::Impl::run(), обведен красным). Запуск цепочки обработки (загрузка файла, применение фильтров).
  • GUI-поток (filterResult). Установка обновленных виджетов, уведомление эскизов для обновления.
  • Фоновый кеш-поток. (ThumbnailPixmapCache::Impl::run(), обведен красным). Загрузка картинок для эскизов и уведомление GUI для обновления.

Фоновые потоки сигналят в GUI о завершении через postEvent.

@plzombie say:

этот StageSequence

Это модель для окна со списком фильтров (массив фильтров и доступ к ним по индексу). MainWindow - контролер. Вся возня внутри MainWindow - это синхронизация между тем, что видно в окне, и списком фильтров, которые сидят в StageSequence.

@noobie-iv
Copy link
Member Author

@plzombie say:

IntrusivePtr

@zvezdochiot say:

вывел OutputGenerator из режима const

У этого указателя тоже есть универсальный конструктор от произвольного типа:

template<typename OT>
    IntrusivePtr(IntrusivePtr<OT> const& other);

Как бы это не повторение истории с CachingFactory, когда этот конструктор вместо конструктора копии срабатывал. Там тоже const/не-const переключал выбор версии конструктора.

@zvezdochiot
Copy link
Member

@plzombie say:

Как бы это не повторение истории с CachingFactory

Ежели это подтвердится, я удалю все мерики и уберу сртировку из Output. И без них обойтись смогу.

@plzombie
Copy link
Contributor

plzombie commented Jan 7, 2025

Сделал debug сборку. Постараюсь пояснить, что за евпаторий там происходит:

Внутри ЧётотамOptionsWidget->setWindowTitle() вызывается

    QString oldAccessibleName;
    const QAccessibleInterface *accessible = QAccessible::queryAccessibleInterface(this);
    if (accessible)
        oldAccessibleName = accessible->text(QAccessible::Name);

Дальше внутри accessible->text(QAccessible::Name) происходит проверка

if (!widget()->accessibleName().isEmpty()) {}

Но вот в чём проблема - widget() возвращает нам nullptr
изображение

Ну и вот вопрос - какого хрена?

@plzombie
Copy link
Contributor

plzombie commented Jan 7, 2025

Ну а вот тут баги всякие

https://bugreports.qt.io/browse/QTBUG-95134
https://bugreports.qt.io/browse/QTBUG-82445

https://bugreports.qt.io/browse/QTBUG-83387 со ссылкой на https://bugreports.qt.io/browse/QTBUG-108637
вот

Maybe it should be deprecated in favour of a disconnectAll() which makes this behaviour more explicit. Or would it make sense to add a runtime warning (for debug builds) in case the object to be disconnected is a QWidget/QQuickItem?

Doing a quick git grep "disconnect()" in the Qt codebase (qtbase) shows that it's rarely used outside the tests. For user code it is most likely simply wrong to use that method.

и вот

As a result of the discussion in QTBUG-83387 it was realized that the invocation of the disconnect all Signals Method in QObject can cause unwanted side effects and even crashes.

Known crashes occur in context of the Qt accessibility implementation on Windows, where QObjects are tracked via the destroyed signal und disconnecting all signals causes dangling pointers.

I therefore suggest to deprecate this method, to add a more explicit disconnectAll method instead and maybe to add runtime warnings when used on QWidget/QQuickItem objects.

The side effects are documented, but using the "disconnect all" functionality is just too tempting for developers so the API should help to prevent issues like this.

Ну и прямо внутри SafeDeletingQObjectPtr мы видим что...? Правильно, m_pObj->disconnect();

Мои предложения - протестировать на линуксе с версией >=6.8 (есть в готовящемся выпуске Fedora 42, например). Если не воспроизводится - то смотреть дальше в сторону disconnect(), собирать статистику, можно ли воспроизвести ошибку в <=6.7.3

@plzombie
Copy link
Contributor

plzombie commented Jan 7, 2025

Вот прямо конкретно объяснение ошибки:

Qt maintains a cache (see QAccessibleCache) for accessible implementation and uses the QObject::destroyed signal to clean up the cache accordingly if a widget is deleted. When disconnecting the signals of a widget, like the customButton in my case, and using disconnect(customButton, 0, 0, 0) or customButton->disconnect() the QObject::destroyed signal which originally connected in the QAccessibleCache will be also disconnected. When then performing the delete customButton there will be no chance any more for the QAccessibleCache to clean up things. And if now you go on with creating and destroying widgets in this manner then there are references to old objects in the QAccessibleCache which can lead to a crash.
In my case I could fix the issue by changing the disconnect(customButton, 0, 0, 0) to disconnect(customButton, 0, this, 0) because in my case it was only important to disconnect the signals which are connected to this because in my case this is the manager class for the buttons and with that kind of disconnect the signal QObject::destroyed is not disconnected and hence the QAccessibleCache can update the cache accordingly if the customButton is destroyed.

You might search in the source code of your application for

disconnect()
disconnect(xxx, 0, 0, 0) where xxx is the application specific object name
disconnect(xxx) where xxx is the application specific object name

for objects of type QWidget or derived from QWidget and try to replace those disconnect calls with a disconnect e.g. specifying from which receiver the signals should be disconnected, e.g. disconnect(myObject, 0, myManager, 0)

@zvezdochiot
Copy link
Member

zvezdochiot commented Jan 8, 2025

@plzombie , хмм... я отключал сигналы только на выбор доп. фильтров, чтобы избежать запуска обновления изображения (фильтры то с текущими параметрами уже применены и обновления изображения не требуется). Только надо вспомнить, как я их отключал...

Как то так:

colorFilterSize->blockSignals(true);
colorFilterCoef->blockSignals(true);

colorFilterSize->blockSignals(false);
colorFilterCoef->blockSignals(false);

PS: Но были отключения и в page_layout, но это уже до меня.

@plzombie
Copy link
Contributor

plzombie commented Jan 8, 2025

Короче вот https://github.com/ImageProcessing-ElectronicPublications/scantailor-experimental/tree/fix_issue_51
Заменил disconnect() на виртуальную функцию disconnectAll(), где уже все сигналы конкретного OptionsWidget перечисляются.
Но в процессе тестирования заметил, что там и так все сигналы отсоединяются (в MainWindow, например, есть код)
Хотя, возможно, и я где-то накосячил.
С blockSignals не знаю что делать, надо читать доки. У меня, как минимум, больше не падает сборка.

@plzombie
Copy link
Contributor

plzombie commented Jan 8, 2025

Ссылка на билд https://disk.yandex.ru/d/asJpyv4mwb4Taw

zvezdochiot added a commit that referenced this issue Jan 8, 2025
@zvezdochiot zvezdochiot added the complete Performed label Jan 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working complete Performed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants