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
|