Skip to content
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

Prevent post-compilation Qt downgrades #2576

Merged
merged 3 commits into from
Jan 25, 2019
Merged

Prevent post-compilation Qt downgrades #2576

merged 3 commits into from
Jan 25, 2019

Conversation

brainplot
Copy link
Contributor

Require the same version of Qt the app was compiled against (or higher).

Type of change

  • ✅ Bug fix (non-breaking change which fixes an issue)

Description and Context

Since the Qt framework is dynamically linked to the app, it may happen that the app is compiled against a version of Qt higher than the one it'll be running with.
Snippets of code that are added/selected at compile time, such as this:

#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
    QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif

will break if the app is compiled against, say, Qt 5.11 and then run in an environment with Qt 5.2 (which is the minimum supported version according to the build instructions).

This patch stops the execution of the app right away if a situation like this occurs.

Testing strategy

I simply ran all existing tests.

Checklist:

  • ✅ I have read the CONTRIBUTING document. [REQUIRED]
  • ✅ My code follows the code style of this project. [REQUIRED]
  • ✅ All new and existing tests passed. [REQUIRED]
  • ✅ I have compiled and verified my code with -DWITH_ASAN=ON. [REQUIRED]

@droidmonkey
Copy link
Member

I'm on the fence about this, but great points regarding running old Qt API. Would it be possible to wrap those higher API calls in a runtime version check (in addition to the compile-time check)?

@brainplot
Copy link
Contributor Author

I'm not aware of a method to check which Qt version is installed on the machine at runtime.
I tried googling and I found a couple of posts suggesting to parse the output of qmake -v, but that feels a bit hacky to me; and probably won't work on Windows machines. Not to mention that it might be relatively slow.

@droidmonkey
Copy link
Member

That macro itself is pulling the current run version using qVersion() which returns the version number as a const char*.

@brainplot
Copy link
Contributor Author

brainplot commented Jan 4, 2019

So I can make every compile time check wrap a corresponding run-time check using qVersion(). How about that?

@droidmonkey
Copy link
Member

droidmonkey commented Jan 4, 2019

Since Qt doesn't make this very easy, I think you should do a one-time conversion of the qVersion() output into an integer that can be easily compared against the QT_VERSION_CHECK macro construction:

QString sq = QString::fromLatin1(qVersion()); \
int version = (sq.section(QChar::fromLatin1('.'),0,0).toInt()<<16) + (sq.section(QChar::fromLatin1('.'),1,1).toInt()<<8)+ sq.section(QChar::fromLatin1('.'),2,2).toInt()

This could be stored in the config class (ie, config()->qtVersion()) or config()->checkQtVersion(major, minor, patch)

@brainplot
Copy link
Contributor Author

Alright, I think I can add this. Shall I keep the compile time checks wrapping the run-time checks or replace all compile time checks with run-time checks?

Keeping the compile time checks as a wrapper would imply that the newer code would be cut off at compile time and the user will never be able to benefit from it, even if the app will be run with a higher version of Qt at a later time.

@droidmonkey
Copy link
Member

We need to keep the compile time checks so people can build using our posted minimum Qt version of 5.2.

@brainplot
Copy link
Contributor Author

brainplot commented Jan 5, 2019

I ended up putting the function in Tools.cpp because I thought it's more appropriate than Config.cpp, which takes care of the user preferences.

I replaced a couple of compile time checks with runtime checks, namely the workaround for Unity in MainWindow.cpp. I also slightly changed that code so that it checks if the desktop environment is actually Unity prior to applying the workaround, as opposed to applying it only based on the Qt version and regardless of the desktop environment.

EDIT: I made the changes because of your request but I feel like the code has become harder to maintain now. Also, I don't think it's idiomatic Qt checking for the version at runtime. Also, checking for the Qt version at runtime when using deprecated methods, effectively broke the dev build because the compiler can't prove that the "old" methods will be used only when a lower version of Qt is detected at runtime, so I think we should either gracefully exit if the app is launched with a lower version of Qt or leave the code as before. Just my opinion.

@droidmonkey
Copy link
Member

droidmonkey commented Jan 16, 2019

This is a common practice in the Android world due to the "fragmentation" issue. I feel like it should be common in Qt as well since they made so many API changes between 5.2 and 5.12.

I am going to sit on this until @phoerious reviews.

@brainplot
Copy link
Contributor Author

I understand. If we're going to use the runtime-check approach, I think I'm going to refactor this to use the QVersionNumber class, which I've just found out about. It seems to make this task more natural and less "hacky", thanks to the fromString and compare methods.
I'll also add some #pragma statements over the methods which have been deprecated, in order to tell the compiler to ignore those and restore the dev build.

@droidmonkey
Copy link
Member

QVersionNumber was introduced in Qt 5.6

@brainplot
Copy link
Contributor Author

Yeah, I hadn't noticed that before. I only added the two #pragma statements to get the dev build working again.

@phoerious
Copy link
Member

I still don't understand why somebody would downgrade from 5.11 to 5.2 at runtime. That should never happen. Version upgrades may occur, but shouldn't break anything since we are not using depcrated functionality.

@brainplot
Copy link
Contributor Author

brainplot commented Jan 17, 2019

Well, it doesn't seem so far-fetched to me that I may have compiled KeePassXC on my machine which has, say, Qt 5.12.0, and then I take the compiled binary to my friend's computer which only has Qt 5.6.0.
Now, I didn't actually try this but if something like this happens, the program will probably crash since it'll try to reference non-existent functions. That was my point with this PR; I'm sorry if that wasn't clear from my explanation. I've never meant downgrading Qt at runtime while the program is running; that of course should never happen!

I was just proposing a way to handle this case gracefully instead of just letting the app crash on the user.

@phoerious
Copy link
Member

Qt versions are not necessarily binary-compatible across major versions, so you should never do that. Use our AppImage instead.

@brainplot
Copy link
Contributor Author

I understand. Thanks :)

@droidmonkey
Copy link
Member

I think this PR is still useful, do we want to merge it in?

@brainplot
Copy link
Contributor Author

brainplot commented Jan 21, 2019

Personally I'm not too convinced. I think the static approach of aborting during startup is a fairer approach. Especially since, as @phoerious says, downgrading Qt is something that should never happen and the goal was to handle gracefully an event that should be extremely exceptional (well, at least more gracefully than a crash).
Penalizing users with runtime overhead for something like this feels a bit overkill.

@droidmonkey
Copy link
Member

OK, I agree with that. If you want to keep your QtVersionChecker function in that is fine by me. Otherwise you can revert back to the graceful exit approach.

@brainplot
Copy link
Contributor Author

brainplot commented Jan 21, 2019

Ok, what about this change? I'll restore the compile time check so that this entire block of code won't be dumped to the output binary at all if the user compiles with Qt >= 5.9 and I'll also leave the runtime check so that the fix applies only if needed (just in case the user does have Qt <= 5.9 at compile time but upgrades later on and the workaround is no longer necessary); if you're okay with that.

@@ -987,16 +993,21 @@ void MainWindow::toggleWindow()
     } else {
         bringToFront();
 
-#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) && (QT_VERSION < QT_VERSION_CHECK(5, 9, 0))
+#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS)
         // re-register global D-Bus menu (needed on Ubuntu with Unity)
         // see https://github.com/keepassxreboot/keepassxc/issues/271
         // and https://bugreports.qt.io/browse/QTBUG-58723
         // check for !isVisible(), because isNativeMenuBar() does not work with appmenu-qt5
-        if (!m_ui->menubar->isVisible()) {
-            QDBusMessage msg = QDBusMessage::createMethodCall("com.canonical.AppMenu.Registrar",
-                                                              "/com/canonical/AppMenu/Registrar",
-                                                              "com.canonical.AppMenu.Registrar",
-                                                              "RegisterWindow");
+        const static auto isDesktopSessionUnity = QString::fromLatin1(qgetenv("XDG_SESSION_DESKTOP"))
+                                                    .contains(QStringLiteral("unity"), Qt::CaseInsensitive);
+
+        if (Tools::qtRuntimeVersion() < QT_VERSION_CHECK(5, 9, 0)
+                && isDesktopSessionUnity
+                && !m_ui->menubar->isVisible()) {
+            QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("com.canonical.AppMenu.Registrar"),
+                                                              QStringLiteral("/com/canonical/AppMenu/Registrar"),
+                                                              QStringLiteral("com.canonical.AppMenu.Registrar"),
+                                                              QStringLiteral("RegisterWindow"));

@droidmonkey
Copy link
Member

Good for me

Gianluca Recchia added 2 commits January 22, 2019 11:33
The app will now exit immediately if it was compiled with a Qt version
higher than the one present on the machine.
@brainplot
Copy link
Contributor Author

Alright, it should be ok now.

src/core/Tools.h Show resolved Hide resolved
@droidmonkey droidmonkey added this to the v2.4.0 milestone Jan 22, 2019
@brainplot brainplot changed the title Prevent Qt downgrades at runtime Prevent post-compilation Qt downgrades Jan 22, 2019
@droidmonkey droidmonkey merged commit 395a88a into keepassxreboot:develop Jan 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants