d1vanov / quentier

Cross-platform desktop Evernote client
GNU General Public License v3.0
134 stars 16 forks source link

Introduce self-update mechanism #150

Closed d1vanov closed 4 years ago

d1vanov commented 6 years ago

Quentier should be able to check itself for updates, as pretty much all other desktop apps do. However, the self-update functionality only needs to be enabled for bundled builds i.e. for those having -DCREATE_BUNDLE=ON.

It seems the mechanisms for self-update checks need to be different for Windows/Mac versions and Linux AppImage as AppImage project has the proposed solution for self-update functionality.

antony-jr commented 6 years ago

@d1vanov The official AppImageUpdate library is written in C++ and its here , the author written a Qt Widget with the C++ library. If you like you can try my library which is Single threaded and Non-Blocking using Qt's Event Loop and its here , For now I want some insight for my library , Its good but I want some pro's insight(like yourself) , My library's documentation is here.

Example

#include <QApplication>
#include <AppImageUpdaterBridge>

using AppImageUpdaterBridge::AppImageDeltaRevisioner;

int main(int ac , char **av)
{
    QApplication app(ac , av);
    /*  If you don't give any paths , The current AppImage is automatically guessed. */
    AppImageDeltaRevisioner  Revisioner;
    Revisioner.setShowLog(true);
    QObject::connect(&Revisioner , &AppImageDeltaRevisioner::finished , 
     [&](QJsonObject newVersionDetails , QString oldVersionPath){
     qInfo() << "New Version:: " << newVersionDetails;
     qInfo() << "Old Version Path:: " << oldVersionPath; /*This AppImage Absolute Path. */
    });
    /* You can also connect a lot of other cool signals , 
        Please refer the docs mentioned above.*/
    Revisiioner.start(); // Start the update.
    return app.exec();
}

Also see this example application which re implements the AppImageUpdate tool in pure Qt and C++.

Note: The update information is automatically extracted from the AppImage using the embeded update information which is written by linuxdeployqt or appimage-tool at travis-ci build.

Ping me if you want some help , I can send you a PR if you want to use AppImageUpdaterBridge , I'm not quite sure how to use AppImageUpdate official library since it lacks documentation and pure support for Qt's event loop.

d1vanov commented 6 years ago

Thank you for the library suggestion, I'll take a look into the available options when I'm ready to start working on this issue.

antony-jr commented 5 years ago

Just letting you know , Now you can deploy updater for your Application with zero code change. It only takes about 800 KiB.

Here is a small script which repacks the latest Quentier AppImage along with this updater.(Run this script in a separate directory)

#!/usr/bin/env bash

# Download Quentier
wget -c "https://github.com/d1vanov/quentier/releases/download/continuous-master/Quentier-master-x86_64.AppImage"
chmod +x Quentier-master-x86_64.AppImage

# Extract
./Quentier-master-x86_64.AppImage --appimage-extract

# Configure Updater
cat > updatedeployqt.json <<EOF
{
   // Always use AppImageUpdater for AppImages
   "bridge" : "AppImageUpdater",

   // Use this to comply with GDPR
   "manual-update-check" : {
       // The QObject name where 'Check for Update' has to integrated
       "qmenu-name" : "menuHelp"   
   }
}
EOF

# Deploy Updater
wget -O updatedeployqt-x86_64.AppImage "https://git.io/fj4CH"
chmod +x updatedeployqt-x86_64.AppImage
./updatedeployqt-x86_64.AppImage squashfs-root/

# Recreate
wget -c "https://github.com/AppImage/AppImageKit/releases/download/12/appimagetool-x86_64.AppImage"
chmod +x appimagetool-x86_64.AppImage

# Please change the update info because the tag is wrong.
./appimagetool-x86_64.AppImage -u "gh-releases-zsync|d1vanov|quentier|continuous-master|Quentier*-x86_64.AppImage.zsync" --no-appstream squashfs-root/

# Cleanup
rm -rf squashfs-root/
rm -rf *zsync

It should look something like this ,

Screenshot_20190607_105001 Screenshot_20190607_105020 Screenshot_20190607_105035 Screenshot_20190607_105108 Screenshot_20190607_105125

d1vanov commented 5 years ago

Thanks for the heads up, looks pretty cool :+1: I might use this for the AppImage version of Quentier but I'd also need some update mechanism for Mac .app and for Windows installation. While I can have different update mechanisms for all these types of installations, it seems at least some common code above these would be required. So it's more likely I'd end up using the library explicitly within the app.

d1vanov commented 4 years ago

The time has come to actually do something here. It's a shame the app has no ability whatsoever to check whether a new version of it exists.

There are several scenarios in which I need the ability to check for updates:

  1. AppImage
  2. Windows installation or portable package
  3. macOS .app

For AppImage it is possible to implement downloading the update from within the app - it looks rather good and I should probably implement it. For Windows and Mac versions I think for now it would be enough to route users to the appropriate GitHub release page so they can download the new version themselves.

Need to add some configurability for updates checking: the app should offer at least two different update channels: stable (master) or unstable (development) ones. The default should depend on the branch from which the app is built. Need to figure out how it would play with AppImage and its zsync files.

antony-jr commented 4 years ago

For Windows and Mac versions I think for now it would be enough to route users to the appropriate GitHub release page so they can download the new version themselves.

For windows you could use Qt Installer Framework for creating the setup and promoting updates.

d1vanov commented 4 years ago

I had a brief look at Qt Installer Framework a while ago and wasn't very impressed. I have a feeling it was developed to fulfill Qt's own agenda but not for any general piece of software. People still manage to use it for general software too but it might require horrible hacks, for example, like this one.

I should probably take another look, maybe the framework has evolved and things have become easier now for general software too, but, to be honest, I think creating a simple notifier about the available update redirecting to the download page would work for Windows and Mac. This is the approach used by a number of apps, e.g. qbittorrent.

d1vanov commented 4 years ago

Note for myself: what's still left to be implemented:

  1. Slots in MainWindow for update checking related signals from PreferencesDialog.
  2. Setters for update options and their proper implementations in UpdateManager.
d1vanov commented 4 years ago

Tested checking for updates via GitHub releases on Windows. It seems to work in general but there's one problem: each time after a check for update was made the app crashes on quit. Breakpad's stack trace shows nothing even remotely related to Quentier or its libraries, it seems something is crashing somewhere deep down in the gory guts of Qt. Can't reproduce the same on Linux so far which makes me think it's some Qt bug. Need to find some way around it.

d1vanov commented 4 years ago

Perhaps I should try not using QNetworkReply directly in NetworkReplyFetcher but instead use QNetworkAccessManager's signals: https://www.bogotobogo.com/Qt/Qt5_Downloading_Files_QNetworkAccessManager_QNetworkRequest.php. For this to work I'd need to remove NetworkReplyFetcher's constructor taking a pointer to external QNetworkAccessManager instance and adapt the code in wiki tools.

d1vanov commented 4 years ago

Trying to rewrite using QNetworkAccessManager instead of QNetworkReply directly did not help the crashes on exit. On the other hand, it seems crashes don't occur with builds against Qt 5.14. If I don't find any better way to deal with this problem, I might need to update Qt used by Quentier Windows builds from 5.13.2 to 5.14.

d1vanov commented 4 years ago

So I spent a week trying to debug the mysterious crashes on exiting the app on Windows only after it has checked the presence of updates and I've made very little progress. I was able to get a decent stacktrace of the crash:

Not Flagged >   5988    0   Main Thread Main Thread [Inline Frame] Qt5Network.dll!QExplicitlySharedDataPointer<QNetworkConfigurationPrivate>::{dtor}
                        [Inline Frame] Qt5Network.dll!QExplicitlySharedDataPointer<QNetworkConfigurationPrivate>::{dtor}() Line 182
                        Qt5Network.dll!QNetworkConfiguration::~QNetworkConfiguration() Line 229
                        Qt5Core.dll!QHashData::free_helper(void(*)(QHashData::Node *) node_delete) Line 573
                        [Inline Frame] Qt5Network.dll!QHash<QNetworkConfiguration,QWeakPointer<QNetworkSession> >::freeData(QHashData *) Line 587
                        [Inline Frame] Qt5Network.dll!QHash<QNetworkConfiguration,QWeakPointer<QNetworkSession> >::{dtor}() Line 254
                        [Inline Frame] Qt5Network.dll!qThreadStorage_deleteData(void *) Line 92
                        Qt5Network.dll!QThreadStorage<QSharedNetworkSessionManager *>::deleteData(void * x) Line 135
                        Qt5Core.dll!QThreadStorageData::finish(void * * p) Line 202
                        Qt5Core.dll!QCoreApplicationPrivate::cleanupThreadData() Line 525
                        Qt5Gui.dll!QGuiApplicationPrivate::~QGuiApplicationPrivate() Line 1624
                        [External Code]
                        [Inline Frame] Qt5Core.dll!QScopedPointerDeleter<QObjectData>::cleanup(QObjectData *) Line 60
                        [Inline Frame] Qt5Core.dll!QScopedPointer<QObjectData,QScopedPointerDeleter<QObjectData> >::{dtor}() Line 107
                        Qt5Core.dll!QObject::~QObject() Line 1044
                        quentier.exe!main(int argc, char * * argv) Line 201
                        [External Code]

Searching for Qt bugs about something like this hasn't yielded anything fruitful for now.

d1vanov commented 4 years ago

Since this thing fires only when user quits the app and only on Windows, I decided to do the following: before the application exits I unregister breakpad's exception handler and use some WinAPI magic found on StackOverflow to disable Windows error reporting dialog in case of crashes. The end user shouldn't notice any problems this way. I've tested this approach and it seems to work.

It is a horrible hack but it seems to be the best thing I can do in these circumstances. Actually, I'm not the first to come up with a hack like that.

antony-jr commented 4 years ago

@d1vanov I suggest you to use Qt installer framework if you resort to this type of hack. You can actually create your own updater like in your case with Qt installer framework too. With something like this https://github.com/antony-jr/QInstallerBridge which I experimented way back when I had the same problem as yours.

d1vanov commented 4 years ago

Thanks, you've already suggested it in this very ticket :smiley: For now a simple check for update offering a URL for updates download should be enough. Maybe I'll think about using the installer framework in future.

d1vanov commented 4 years ago

It's hard to believe it but it seems I finally got all the functionality I intended to add working; 3.5 months have passed since I started working on this to the current moment. It's a lot of time, even compared to other major features I was working on.

I intend to do some overall testing and bugfixing to stabilize the development branch in order to merge it to master in the near future. Then this update checking functionality would become available in master branch too, for now it's only available in development.