From 4e9e0ef2bc8a11398cb40f4099608e3acb31a07c Mon Sep 17 00:00:00 2001
From: Alexis Lopez Zubieta <contact@azubieta.net>
Date: Mon, 2 Sep 2019 17:19:13 -0500
Subject: [PATCH] Add updater dialog

---
 app/app.pri                              |  10 +-
 app/src/main.cpp                         |  17 ++-
 app/src/updaters/appimageupdatedialog.ui |  96 +++++++++++++++
 app/src/updaters/appupdatedialog.cpp     |  67 +++++++++++
 app/src/updaters/appupdatedialog.h       |  44 +++++++
 app/src/updaters/appupdater.cpp          | 144 +++++++++++++++++++++++
 app/src/updaters/appupdater.h            |  55 +++++++++
 7 files changed, 429 insertions(+), 4 deletions(-)
 create mode 100644 app/src/updaters/appimageupdatedialog.ui
 create mode 100644 app/src/updaters/appupdatedialog.cpp
 create mode 100644 app/src/updaters/appupdatedialog.h
 create mode 100644 app/src/updaters/appupdater.cpp
 create mode 100644 app/src/updaters/appupdater.h

diff --git a/app/app.pri b/app/app.pri
index 5f750fe..ab4a8ed 100644
--- a/app/app.pri
+++ b/app/app.pri
@@ -4,18 +4,23 @@ QT += \
     core \
     gui \
     network \
+    widgets \
     websockets
 
 HEADERS += \
+    $${PWD}/src/updaters/appupdatedialog.h \
     $${PWD}/src/websockets/websocketserver.h \
     $${PWD}/src/handlers/confighandler.h \
     $${PWD}/src/handlers/systemhandler.h \
     $${PWD}/src/handlers/ocsapihandler.h \
     $${PWD}/src/handlers/itemhandler.h \
     $${PWD}/src/handlers/updatehandler.h \
-    $${PWD}/src/handlers/desktopthemehandler.h
+    $${PWD}/src/handlers/desktopthemehandler.h \
+    $${PWD}/src/updaters/appupdater.h
 
 SOURCES += \
+    $${PWD}/src/updaters/appupdatedialog.cpp \
+    $${PWD}/src/updaters/appupdater.cpp \
     $${PWD}/src/main.cpp \
     $${PWD}/src/websockets/websocketserver.cpp \
     $${PWD}/src/handlers/confighandler.cpp \
@@ -48,3 +53,6 @@ contains(DEFINES, APP_DESKTOP) {
         $${PWD}/src/desktopthemes/cinnamontheme.cpp \
         $${PWD}/src/desktopthemes/matetheme.cpp
 }
+
+FORMS += \
+    $$PWD/src/updaters/appimageupdatedialog.ui
diff --git a/app/src/main.cpp b/app/src/main.cpp
index 6f03743..34b5851 100644
--- a/app/src/main.cpp
+++ b/app/src/main.cpp
@@ -4,17 +4,18 @@
 #include <QLocale>
 #include <QCommandLineParser>
 #include <QCommandLineOption>
-#include <QGuiApplication>
+#include <QApplication>
 #include <QIcon>
 #include <QDebug>
 
 #include "handlers/confighandler.h"
 #include "websockets/websocketserver.h"
+#include "updaters/appupdater.h"
 
 int main(int argc, char *argv[])
 {
     // Init
-    QGuiApplication app(argc, argv); // This is backend program, but need GUI module
+    QApplication app(argc, argv); // This is backend program, but need GUI module
 
     auto envPath = QString::fromLocal8Bit(qgetenv("PATH").constData()) + ":" + app.applicationDirPath();
     qputenv("PATH", envPath.toUtf8().constData());
@@ -27,6 +28,7 @@ int main(int argc, char *argv[])
     app.setOrganizationName(appConfigApplication["organization"].toString());
     app.setOrganizationDomain(appConfigApplication["domain"].toString());
     app.setWindowIcon(QIcon::fromTheme(appConfigApplication["id"].toString(), QIcon(appConfigApplication["icon"].toString())));
+    app.setQuitOnLastWindowClosed(false);
 
     // Setup translator
     QTranslator translator;
@@ -45,13 +47,22 @@ int main(int argc, char *argv[])
     QCommandLineOption clOptionPort(QStringList() << "p" << "port", "Port for websocket server [default: 49152].", "port", "49152");
     clParser.addOption(clOptionPort);
 
+    QCommandLineOption clOptionAppPath(QStringList() << "a" << "appFile", "Path to the main AppImage <file>.", "file");
+    clParser.addOption(clOptionAppPath);
+
     clParser.process(app);
 
+    // Setup AppUpdater
+    auto appFile = clParser.value(clOptionAppPath);
+    AppUpdater appUpdater(appFile);
+    appUpdater.setSilentLookup(true);
+    appUpdater.doUpdateLookUp();
+
     auto port = clParser.value(clOptionPort).toUShort();
 
     // Setup websocket server
     auto *wsServer = new WebSocketServer(configHandler, appConfigApplication["id"].toString(), port, &app);
-    QObject::connect(wsServer, &WebSocketServer::stopped, &app, &QGuiApplication::quit);
+    QObject::connect(wsServer, &WebSocketServer::stopped, &app, &QGuiApplication::quit);    
 
     if (wsServer->start()) {
         qInfo() << "Websocket server started at:" << wsServer->serverUrl().toString();
diff --git a/app/src/updaters/appimageupdatedialog.ui b/app/src/updaters/appimageupdatedialog.ui
new file mode 100644
index 0000000..70fe2ab
--- /dev/null
+++ b/app/src/updaters/appimageupdatedialog.ui
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AppImageUpdateDialog</class>
+ <widget class="QDialog" name="AppImageUpdateDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>405</width>
+    <height>126</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Pling Store Updater</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QStackedWidget" name="stackedWidget">
+     <widget class="QWidget" name="confirmationPage">
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <property name="spacing">
+        <number>12</number>
+       </property>
+       <item>
+        <widget class="QLabel" name="confirmationLabel">
+         <property name="text">
+          <string>A new version of Pling-Store is available!
+
+Do you want to download it?</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout">
+         <item>
+          <widget class="QPushButton" name="latterButton">
+           <property name="text">
+            <string>Latter</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item>
+          <widget class="QPushButton" name="doitButton">
+           <property name="text">
+            <string>Update</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="progressPage">
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QLabel" name="title">
+         <property name="text">
+          <string>Downloading update contents</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QProgressBar" name="progressBar">
+         <property name="value">
+          <number>0</number>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/app/src/updaters/appupdatedialog.cpp b/app/src/updaters/appupdatedialog.cpp
new file mode 100644
index 0000000..ae70f3d
--- /dev/null
+++ b/app/src/updaters/appupdatedialog.cpp
@@ -0,0 +1,67 @@
+#include <QDebug>
+
+#include "appupdatedialog.h"
+#include "ui_appimageupdatedialog.h"
+
+AppUpdateDialog::AppUpdateDialog(QWidget *parent) :
+    QDialog(parent),
+    ui(new Ui::AppImageUpdateDialog)
+{
+    ui->setupUi(this);
+    connect(ui->latterButton, &QPushButton::clicked, this, &AppUpdateDialog::reject);
+}
+
+AppUpdateDialog::~AppUpdateDialog()
+{
+    delete ui;
+}
+
+void AppUpdateDialog::showUpdateConfirmationMessage()
+{
+    setWindowTitle(tr("Pling Store Update Available"));
+    ui->confirmationLabel->setText(tr("Do you want to update now to the new Pling Store version now?"));
+    ui->doitButton->setText("Yes");
+
+    disconnect(ui->doitButton, nullptr, this, nullptr);
+    connect(ui->doitButton, &QPushButton::released, this, &AppUpdateDialog::updateRequested);
+
+    ui->stackedWidget->setCurrentWidget(ui->confirmationPage);
+    show();
+}
+
+void AppUpdateDialog::showErrorMessage(const QString &msg)
+{
+    setWindowTitle(tr("Pling Store Update Failed"));
+    ui->confirmationLabel->setText(tr("Do you want to try again?"));
+    ui->doitButton->setText("Yes");
+
+    disconnect(ui->doitButton, nullptr, this, nullptr);
+    connect(ui->doitButton, &QPushButton::released, this, &AppUpdateDialog::updateRequested);
+
+    ui->stackedWidget->setCurrentWidget(ui->confirmationPage);
+    show();
+}
+
+void AppUpdateDialog::showCompletionMessage()
+{
+    setWindowTitle(tr("Plign Store Update Completed"));
+    ui->confirmationLabel->setText(tr("Do you want to open the new version now?"));
+    ui->doitButton->setText("Yes");
+
+    disconnect(ui->doitButton, nullptr, this, nullptr);
+    connect(ui->doitButton, &QPushButton::released, this, &AppUpdateDialog::restartRequested);
+
+    ui->stackedWidget->setCurrentWidget(ui->confirmationPage);
+    show();
+}
+
+void AppUpdateDialog::showProgress(int progress)
+{
+    setWindowTitle(tr("Pling Store Update"));
+
+    ui->progressBar->setValue(progress);
+    ui->progressPage->show();
+
+    ui->stackedWidget->setCurrentWidget(ui->progressPage);
+    show();
+}
diff --git a/app/src/updaters/appupdatedialog.h b/app/src/updaters/appupdatedialog.h
new file mode 100644
index 0000000..6a39204
--- /dev/null
+++ b/app/src/updaters/appupdatedialog.h
@@ -0,0 +1,44 @@
+#ifndef APPIMAGEUPDATEDIALOG_H
+#define APPIMAGEUPDATEDIALOG_H
+
+#include <QDialog>
+
+namespace Ui {
+class AppImageUpdateDialog;
+}
+
+namespace appimage {
+    namespace update  {
+        class Updater;
+    }
+}
+
+class AppUpdateDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit AppUpdateDialog(QWidget *parent = nullptr);
+    ~AppUpdateDialog();
+
+signals:
+    void restartRequested();
+
+    void updateRequested();
+
+public slots:
+    void showUpdateConfirmationMessage();
+
+    void showErrorMessage(const QString &msg);
+
+    void showCompletionMessage();
+
+    void showProgress(int progress);
+
+private:
+    Ui::AppImageUpdateDialog *ui;
+    QString targetAction;
+
+};
+
+#endif // APPIMAGEUPDATEDIALOG_H
diff --git a/app/src/updaters/appupdater.cpp b/app/src/updaters/appupdater.cpp
new file mode 100644
index 0000000..b595248
--- /dev/null
+++ b/app/src/updaters/appupdater.cpp
@@ -0,0 +1,144 @@
+#include <QDebug>
+#include <QtConcurrent/QtConcurrent>
+
+#include "appimage/update.h"
+#include "appupdater.h"
+
+AppUpdater::AppUpdater(const QString &appImagePath, QObject *parent) : QObject(parent), appImagePath(appImagePath), updateHelper(nullptr)
+{
+    connect(this, &AppUpdater::updateAvailable, &updateDialog, &AppUpdateDialog::showUpdateConfirmationMessage);
+    connect(&updateDialog, &AppUpdateDialog::updateRequested, this, &AppUpdater::doUpdate);
+    connect(&updateDialog, &AppUpdateDialog::restartRequested, this, &AppUpdater::doRestart);
+
+    connect(&updateDialog, &AppUpdateDialog::rejected, this, &AppUpdater::stop);
+}
+
+void AppUpdater::setSilentLookup(bool value)
+{
+    silentLookup = value;
+}
+
+void AppUpdater::doUpdateLookUp()
+{
+    if (appImagePath.isEmpty()) {
+        qWarning() << "Self-updates disabled: No app file provided.";
+        return;
+    }
+
+    if (!silentLookup)
+        updateDialog.show();
+
+    QtConcurrent::run([=]() {
+        appimage::update::Updater updater(appImagePath.toStdString());
+
+        bool updateAvailable; // this is an output parameter!!!
+        updater.checkForChanges(updateAvailable);
+
+        if (updateAvailable) {
+            qDebug() << "Update available";
+            emit this->updateAvailable();
+        }
+
+    });
+}
+
+void AppUpdater::doUpdate()
+{
+    if (appImagePath.isEmpty()) {
+        qWarning() << "Self-updates disabled: No app file provided.";
+        return;
+    }
+
+
+    if (updateHelper !=nullptr)
+        delete updateHelper;
+
+    updateHelper = new appimage::update::Updater(appImagePath.toStdString());
+
+    updateHelper->start();
+
+    progressCheckTimer.setInterval(200);
+    progressCheckTimer.start();
+
+    connect(&progressCheckTimer, &QTimer::timeout, this, &AppUpdater::checkUpdateProgress);
+}
+
+void AppUpdater::doRestart()
+{
+    QProcess::startDetached("pkill", {"pling-store"});
+
+    if (updateHelper != nullptr) {
+        std::string pathToNewFile;
+        updateHelper->pathToNewFile(pathToNewFile);
+
+        if (!pathToNewFile.empty()) {
+            QString path = QString::fromStdString(pathToNewFile);
+            QFile::setPermissions(path, QFileDevice::ReadUser | QFileDevice::ExeUser);
+
+
+            QProcess proc;
+            proc.setProgram(path);
+            proc.setEnvironment(getCleanSystemEnvironment());
+
+            qDebug() << proc.environment();
+            if (proc.startDetached()) {
+                updateDialog.accept();
+            } else
+                updateDialog.showErrorMessage("Unable to start: " + path);
+        }
+    }
+}
+
+void AppUpdater::stop()
+{
+}
+
+void AppUpdater::checkUpdateProgress()
+{
+    using namespace appimage::update;
+    auto state = updateHelper->state();
+    switch (state) {
+    case Updater::INITIALIZED:
+        break;
+    case Updater::RUNNING:
+        double progress;
+        updateHelper->progress(progress);
+        updateDialog.showProgress(progress*100);
+        break;
+    case Updater::STOPPING:
+        break;
+    case Updater::SUCCESS:
+        updateDialog.showCompletionMessage();
+        progressCheckTimer.stop();
+        break;
+    case Updater::ERROR:
+        updateDialog.showErrorMessage(tr("Update failed"));
+        progressCheckTimer.stop();
+        break;
+    }
+}
+
+QStringList AppUpdater::getCleanSystemEnvironment()
+{
+    QString appDirPath = qgetenv("APPDIR");
+
+    QProcessEnvironment systenEnvironemnt = QProcessEnvironment::systemEnvironment();
+    QProcessEnvironment  processEnvironment;
+
+    for (QString key: systenEnvironemnt.keys()) {
+        QString value = systenEnvironemnt.value(key);
+
+        QStringList oldValue = value.split(":");
+        QStringList newVaule;
+        for (const QString &valueSection: oldValue)
+            if (!valueSection.contains(appDirPath))
+                newVaule << valueSection;
+
+        if (!newVaule.empty())
+            processEnvironment.insert(key, newVaule.join(":"));
+    }
+
+    return processEnvironment.toStringList();
+//    return {"DISPLAY=:0"};
+}
+
diff --git a/app/src/updaters/appupdater.h b/app/src/updaters/appupdater.h
new file mode 100644
index 0000000..b031191
--- /dev/null
+++ b/app/src/updaters/appupdater.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <QObject>
+#include <QTimer>
+
+#include "appupdatedialog.h"
+
+namespace appimage {
+    namespace update  {
+        class Updater;
+    }
+}
+
+class AppUpdater : public QObject
+{
+    Q_OBJECT
+public:
+    explicit AppUpdater(const QString &appImagePath, QObject *parent = nullptr);
+
+
+    void setSilentLookup(bool value);
+
+signals:
+    void updateAvailable();
+    void restartApp(QString appPath);
+
+public slots:
+
+    void doUpdateLookUp();
+
+    void doUpdate();
+
+    void doRestart();
+
+    void stop();
+
+protected slots:
+    void checkUpdateProgress();
+
+private:
+    /**
+     * @brief Clean reference to APPDIR from the environment
+     * @return
+     */
+    QStringList getCleanSystemEnvironment();
+
+    bool silentLookup;
+
+    QString appImagePath;
+    appimage::update::Updater * updateHelper;
+    AppUpdateDialog updateDialog;
+    QTimer progressCheckTimer;
+
+};
+
-- 
GitLab