File: CppHotPlugActionExtension.cpp

package info (click to toggle)
camitk 6.0.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 389,508 kB
  • sloc: cpp: 103,476; sh: 2,448; python: 1,618; xml: 984; makefile: 128; perl: 84; sed: 20
file content (244 lines) | stat: -rw-r--r-- 10,692 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/

#include "CppHotPlugActionExtension.h"
#include "CppHotPlugAction.h"

#include "HotPlugExtensionManager.h"

#include <TransformEngine.h>
#include <CMakeProjectManager.h>
#include <CamiTKExtensionModel.h>

#include <Log.h>

//-- Qt Stuff
#include <QMessageBox>
#include <QApplication>
#include <QTimer>

namespace camitk {

// -------------------- constructor --------------------
CppHotPlugActionExtension::CppHotPlugActionExtension(const QString& camitkFilePath, bool forceRebuild) : HotPlugActionExtension(camitkFilePath) {
    // setup watch
    connect(&fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, [ =, this](const QString & path) {
        QTimer::singleShot(1000, [ =, this ]() {
            this->extensionRebuilt(path);
        });
    });

    // check if rebuild is required
    CamiTKExtensionModel camitkExtensionModel(camitkFilePath);
    VariantDataModel& data = camitkExtensionModel.getModel();
    if (data["alwaysRebuild"].getValue().toBool() || HotPlugExtensionManager::getVerifyOrRebuildOption() || forceRebuild) {
        CppHotPlugActionExtension::rebuild();
    }
}

// -------------------- destructor --------------------
CppHotPlugActionExtension::~CppHotPlugActionExtension() {
    // make sure nothing is watched anymore
    fileSystemWatcher.removePaths(fileSystemWatcher.files());
}

// -------------------- initActions --------------------
bool CppHotPlugActionExtension::initActions(int progressMinimum, int progressMaximum) {
    CamiTKExtensionModel camitkExtensionModel(getLocation());
    QList<HotPlugAction*> failedActions;

    const VariantDataModel& allActions = camitkExtensionModel.getModel()["actions"];
    for (int i = 0; i < allActions.size(); i++) {
        HotPlugAction* a = new CppHotPlugAction(this, allActions[i]);
        a->init();

        // if the newly created action still needs update now, it means it failed to load its shared library
        if (a->needsUpdate()) {
            failedActions.append(a);
        }
        registerAction(a);
    }

    updateWatchedFiles();

    // warn the user when actions were not successfully loaded
    QStringList failedActionNames;
    for (HotPlugAction* a : failedActions) {
        failedActionNames.append(a->getName());
    }
    QString failedActionMessage;
    if (failedActions.size() == actions.size()) {
        failedActionMessage = QString((actions.size() == 1) ? "the defined action" : "all defined actions") + " (" + failedActionNames.join(", ") + ").";
    }
    else {
        failedActionMessage = "the following actions:<ul><li>" + failedActionNames.join("</li><li>") + "</li></ul>";
    }
    if (failedActions.size() > 0) {
        //-- ask for rebuild when extension dynamic library/shared object
        // Add a timer to auto close the dialog after 30000 to avoid blocking modal dialog during test
        QTimer::singleShot(30000, [ = ]()  {
            QWidget* widget = QApplication::activeModalWidget();
            if (widget) {
                widget->close();
            }
        });
        QMessageBox::StandardButton reply = QMessageBox::warning(nullptr, "Loading HotPlug Action Failed",
                                            tr("Action extension \"%1\" is registered, but an error occurred while loading %2<br/>HotPlug action implementation libraries could not be loaded.<br/>Do you want to rebuild the extension now (this message will automatically close after 30 seconds)?").arg(name).arg(failedActionMessage),
                                            QMessageBox::Yes | QMessageBox::No);
        if (reply == QMessageBox::Yes) {
            rebuild();
            // delete all registered
            while (!actions.empty()) {
                Action* toDelete = actions.takeFirst();
                delete toDelete;
            }
            // retry initialization
            init();
        }
        else {
            // User chose not to rebuild the extension
        }
    }
    else {
        successfullyLoaded = true;
    }

    return successfullyLoaded;
}

// -------------------- updateWatchedFiles --------------------
void CppHotPlugActionExtension::updateWatchedFiles() {
    // see QFileSystemWatcher::fileChanged documentation:
    // Note: As a safety measure, many applications save an open file by writing a new file and then deleting the old one.
    // In your slot function, you can check watcher.files().contains(path). If it returns false, check whether the file still
    // exists and then call addPath() to continue watching it.
    //
    // It seems that is exactly what is happening here. In our case, the application stated in the documentation above
    // is the cmake process (or more precisely nmake, make, msvc, g++, ld,...)
    //
    // If you watch a detailed log of what is happening when an action library is rebuilt while the QFileSystemWatcher is
    // watching the action .so/.dll/.dynlib file (lets call it libmyaction.so from now), you will see that
    // 1. extensionRebuilt is called a first time
    //    - the action is updated using the new libmyaction.so version, the action update the value of lastLoaded
    //    - updateWatchedFiles() is called a first time and nothing has to be done as the file is still in the paths
    //      watched by fileSystemWatcher
    // 2. extensionRebuilt is called a second time
    //    - the action considers that it is up to date (lastLoaded is greater than lastModified)
    //    - updateWatchedFiles() is called a second time, but this time the file seems not in the list of paths
    //      watched by fileSystemWatcher. It is therefore added _again_

    for (auto e : watchedUserActionLibraries.keys()) {
        if (!fileSystemWatcher.files().contains(e)) {
            if (QFileInfo(e).exists()) {
                CAMITK_TRACE(QString("%1 exists but not yet watched: added to watched.").arg(e));
                fileSystemWatcher.addPath(e);
            }
            else {
                CAMITK_TRACE(QString("Action extension \"%1\": file %2 is not available anymore. Not added to watched.").arg(getName()).arg(e));
            }
        }
    }
}

// -------------------- watchActionLibrary --------------------
void CppHotPlugActionExtension::watchActionLibrary(QString libraryPath, HotPlugAction* action) {
    watchedUserActionLibraries.insert(libraryPath, action);
}

// -------------------- extensionRebuilt --------------------
void CppHotPlugActionExtension::extensionRebuilt(const QString& path, int timerCallCount) {
    // this will be hit by the first library watch signal, locks for all, the other thread/signals will pass
    if (reloadMutex.tryLock()) {
        bool updateSucceeded = false;
        HotPlugAction* hotPlugAction = watchedUserActionLibraries.value(path);
        if (hotPlugAction) {
            if (hotPlugAction->needsUpdate()) {
                CAMITK_TRACE(QString("Updating HotPlug Action \"%1\"...").arg(hotPlugAction->getName()));
                updateSucceeded = hotPlugAction->update();
            }
            else {
                updateSucceeded = true;
                CAMITK_TRACE(QString("HotPlug Action \"%1\" does not need any update").arg(hotPlugAction->getName()));
            }
            updateWatchedFiles();
        }
        else {
            CAMITK_INFO(QString("HotPlug Action not found"));
        }

        if (!updateSucceeded && timerCallCount < 2) {
            CAMITK_TRACE(QString("HotPlug Action update failed, retrying in 5 sec..."));
            /// try again in 5 sec as the failure to load the shared library might be due to an incomplete file
            QTimer::singleShot(5000, [ =, this ]() {
                this->extensionRebuilt(path, timerCallCount + 1);
            });
        }

        // unlocking the mutex for watch to be active again
        reloadMutex.unlock();
    }
}

// -------------------- rebuild --------------------
void CppHotPlugActionExtension::rebuild() {
    // remove build
    QString sourcePath = QFileInfo(getLocation()).absolutePath();
    QDir buildExtensionDir;
    buildExtensionDir.setPath(sourcePath + "/build");
    if (buildExtensionDir.exists()) {
        buildExtensionDir.removeRecursively();
    }

    // rebuild now
    CMakeProjectManager manager(getLocation());
    CAMITK_INFO_ALT(QString("Building %1").arg(getLocation()));
    if (!manager.success()) {
        CAMITK_WARNING_ALT(QString("Failed to initialize build for \"%1\"").arg(getLocation()));
    }
    else {
        manager.setStages({CMakeProjectManager::Check_System, CMakeProjectManager::Generate_Source_Files, CMakeProjectManager::Configure_CMake, CMakeProjectManager::Build_Project});

        // connect signal to follow progress
        connect(&manager, &CMakeProjectManager::stageStarted, [ = ](const QString & stage) {
            CAMITK_INFO_ALT(QString("Starting %1...").arg(stage));
        });

        connect(&manager, &CMakeProjectManager::stageFinished, [ = ](const QString & stage, bool success, const QString & output) {
            CAMITK_WARNING_IF_ALT(!success, QString("%1 finished %2").arg(stage).arg((success) ? "[OK]" : "[FAILED]"));
            CAMITK_WARNING_IF_ALT(!success, QString("Output:\n%1\n").arg(output));
            CAMITK_INFO_IF_ALT(success, QString("%1 finished %2").arg(stage).arg((success) ? "[OK]" : "[FAILED]"));
        });

        connect(&manager, &CMakeProjectManager::allStagesFinished, [ this ](bool status) {
            if (!status) {
                CAMITK_WARNING(QString("Building %1 failed.").arg(getLocation()));
            }
        });

        manager.start();
    }
}

} // namespace camitk