diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index d7ad4bcdc2..4b446aae03 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -17,13 +17,14 @@ jobs: runs-on: ubuntu-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + JASP_R_VERSION: '4.5.3' steps: - uses: actions/checkout@v3 - uses: r-lib/actions/setup-r@v2 with: - r-version: 'release' + r-version: ${{ env.JASP_R_VERSION }} - name: Get Installed R Version run: echo "R_VERSION=$(R --version | head -n 1 | awk '{print $3}')" >> $GITHUB_ENV diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index fc5e117b4a..1c4785ed9a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -12,6 +12,7 @@ jobs: runs-on: windows-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + JASP_R_VERSION: '4.5.3' name: build test on Windows @@ -19,7 +20,7 @@ jobs: - uses: r-lib/actions/setup-r@v2 with: - r-version: 'release' + r-version: ${{ env.JASP_R_VERSION }} windows-path-include-rtools: false windows-path-include-mingw: true diff --git a/CMakeLists.txt b/CMakeLists.txt index fe611d9e08..d7c070877a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,8 @@ else() USES_TERMINAL COMMENT "------ Configuring and Building the libR-Interface") + add_dependencies(SyntaxInterface R-Interface) + endif() if(NOT USE_QT_STATIC_LIBS) diff --git a/CommonData/CMakeLists.txt b/CommonData/CMakeLists.txt index 129da25bc6..647f6a0b11 100644 --- a/CommonData/CMakeLists.txt +++ b/CommonData/CMakeLists.txt @@ -60,6 +60,10 @@ target_link_libraries( ) +if(MSVC) + target_compile_options(CommonData PRIVATE /EHsc) +endif() + target_compile_definitions( CommonData PUBLIC $<$:PRINT_ENGINE_MESSAGES> diff --git a/CommonData/archivereader.cpp b/CommonData/archivereader.cpp index ab55da8427..83aa16b5d6 100644 --- a/CommonData/archivereader.cpp +++ b/CommonData/archivereader.cpp @@ -111,6 +111,8 @@ void ArchiveReader::writeEntryToTempFiles(std::function progressCal std::ofstream file(TempFiles::createSpecific("", _entryPath).c_str(), std::ios::out | std::ios::binary); + if (!file.is_open()) + throw runtime_error("Could not open temporary archive entry '" + _entryPath + "' for writing."); static char streamBuff[8192 * 32]; file.rdbuf()->pubsetbuf(streamBuff, sizeof(streamBuff)); //Set the buffer manually to make it much faster our issue https://github.com/jasp-stats/INTERNAL-jasp/issues/436 and solution from: https://stackoverflow.com/a/15177770 @@ -130,10 +132,19 @@ void ArchiveReader::writeEntryToTempFiles(std::function progressCal if(bytes > 0 && errorCode == 0) file.write(copyBuff, bytes); else break; + + if (!file.good()) + throw runtime_error("Could not write temporary archive entry '" + _entryPath + "'."); } while (true); + if (errorCode != 0) + throw runtime_error("Could not read archive entry '" + _entryPath + "'."); + file.flush(); + if (!file.good()) + throw runtime_error("Could not flush temporary archive entry '" + _entryPath + "'."); + file.close(); } diff --git a/CommonData/databaseinterface.cpp b/CommonData/databaseinterface.cpp index 0cf3ee324a..b2209282aa 100644 --- a/CommonData/databaseinterface.cpp +++ b/CommonData/databaseinterface.cpp @@ -2479,7 +2479,7 @@ void DatabaseInterface::close() _dbCheckMutex.unlock(); - while(sqlite3_close(_dbCreated) != SQLITE_OK) + while(_dbCreated && sqlite3_close(_dbCreated) != SQLITE_OK) { std::this_thread::sleep_for(std::chrono::nanoseconds(10000000)); } diff --git a/CommonData/databridge.cpp b/CommonData/databridge.cpp index 9c00494c29..dfb51a11bb 100644 --- a/CommonData/databridge.cpp +++ b/CommonData/databridge.cpp @@ -36,6 +36,12 @@ DataBridge::DataBridge(unsigned long sessionID, bool useMemory) } } +DataBridge::~DataBridge() +{ + delete _dataSet; + _dataSet = nullptr; +} + void DataBridge::provideStateFileName(std::string & root, std::string & relativePath) { return TempFiles::createSpecific("state", _analysisId, root, relativePath); diff --git a/CommonData/databridge.h b/CommonData/databridge.h index 8d7a179355..0f0c2b6634 100644 --- a/CommonData/databridge.h +++ b/CommonData/databridge.h @@ -25,6 +25,11 @@ class DataBridge { public: DataBridge(unsigned long sessionID, bool useMemory = false); + ~DataBridge(); + DataBridge(const DataBridge &) = delete; + DataBridge & operator=(const DataBridge &) = delete; + DataBridge(DataBridge &&) = delete; + DataBridge & operator=(DataBridge &&) = delete; std::string createColumn( const std::string & columnName, bool computed=false); ///< Returns encoded columnname on success or "" on failure (cause it already exists) bool deleteColumn( const std::string & columnName); diff --git a/CommonData/rbridge.cpp b/CommonData/rbridge.cpp index f22a85840d..d3df6ed30e 100644 --- a/CommonData/rbridge.cpp +++ b/CommonData/rbridge.cpp @@ -64,6 +64,13 @@ size_t _logWriteFunction(const void * buf, size_t len) void rbridge_setDataBridge(DataBridge * dataBridge) { data_bridge = dataBridge; + rbridge_dataSet = nullptr; +} + +void rbridge_clearDataBridge() +{ + data_bridge = nullptr; + rbridge_dataSet = nullptr; } const std::string jaspBaseDistributionSamplersR = @@ -149,6 +156,9 @@ void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, po jaspBaseTransformJohnsonR + "\n" + jaspBaseTransformYeoJohnsonR + "\n" + jaspBaseTransformPowerR; + // SyntaxInterface uses insideJasp=false for parse/dataset replay and only + // needs the native callbacks; the Engine still receives the full R helpers. + const char * initRCodeForMode = insideJasp ? initRCode.c_str() : ""; Log::log() << "Entering jaspRCPP_init." << std::endl; jaspRCPP_init( AppInfo::getBuildYear() .c_str(), @@ -162,7 +172,7 @@ void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, po rbridge_moduleLibraryFixer, resultFont, tempDirStatic.c_str(), - initRCode.c_str(), + initRCodeForMode, insideJasp ); JASPTIMER_STOP(jaspRCPP_init); diff --git a/CommonData/rbridge.h b/CommonData/rbridge.h index a1d4ac19e6..997e23fbd6 100644 --- a/CommonData/rbridge.h +++ b/CommonData/rbridge.h @@ -81,6 +81,7 @@ extern "C" { typedef std::function RCallback; void rbridge_setDataBridge(DataBridge * dataBridge); + void rbridge_clearDataBridge(); void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, ColumnEncoder * encoder, const char * resultFont, bool insideJasp = true); void rbridge_memoryCleaning(); diff --git a/Engine/jaspBase b/Engine/jaspBase index 93f004588f..fc70f1a4f8 160000 --- a/Engine/jaspBase +++ b/Engine/jaspBase @@ -1 +1 @@ -Subproject commit 93f004588f6d56b46d23210e23bff003ae7b1ff4 +Subproject commit fc70f1a4f8687ad226886a57d73239475b124f65 diff --git a/QMLComponents/components/JASP/Controls/GridLayout.qml b/QMLComponents/components/JASP/Controls/GridLayout.qml index 39fcc530ed..af462ff6c0 100644 --- a/QMLComponents/components/JASP/Controls/GridLayout.qml +++ b/QMLComponents/components/JASP/Controls/GridLayout.qml @@ -85,7 +85,7 @@ QT.GridLayout function _checkColumns() { - if (!_initialized || (width === 0)) return; + if (!_initialized || width <= 0 || implicitWidth <= 0 || !isFinite(width) || !isFinite(implicitWidth)) return; if (width < (implicitWidth - 1) && gridLayout.columns >= 2) { diff --git a/QMLComponents/datasetprovider.cpp b/QMLComponents/datasetprovider.cpp index 3be6eaacf1..448a635286 100644 --- a/QMLComponents/datasetprovider.cpp +++ b/QMLComponents/datasetprovider.cpp @@ -20,12 +20,19 @@ #include "utilities/qutils.h" #include "columnencoder.h" +#include + DataSetProvider * DataSetProvider::_singleton = nullptr; DataSetProvider* DataSetProvider::getProvider(bool inMemory, bool reset, QObject* parent) { if (!_singleton) _singleton = new DataSetProvider(inMemory, parent); + else if (_singleton->_inMemory != inMemory) + { + delete _singleton; + _singleton = new DataSetProvider(inMemory, parent); + } else if (reset) _singleton->resetDataSet(); @@ -41,7 +48,7 @@ DataSetProvider::~DataSetProvider() _singleton = nullptr; } -DataSetProvider::DataSetProvider(bool inMemory, QObject *parent) : QAbstractTableModel(parent) +DataSetProvider::DataSetProvider(bool inMemory, QObject *parent) : QAbstractTableModel(parent), _inMemory(inMemory) { _db = new DatabaseInterface(true, inMemory); _dataSet = new DataSet(); @@ -52,6 +59,7 @@ DataSetProvider::DataSetProvider(bool inMemory, QObject *parent) : QAbstractTabl void DataSetProvider::resetDataSet() { + beginResetModel(); if (_dataSet) { _dataSet->dbDelete(); @@ -59,6 +67,7 @@ void DataSetProvider::resetDataSet() } _dataSet = new DataSet(); + endResetModel(); } int DataSetProvider::rowCount(const QModelIndex &) const @@ -113,18 +122,36 @@ void DataSetProvider::loadDataSet(const std::map & data } +void DataSetProvider::closeDatabase() +{ + _db->close(); +} + void DataSetProvider::loadDatabase(const Version & jaspVersion) { + beginResetModel(); delete _dataSet; + _dataSet = nullptr; - _db->close(); - _db->load(); - _db->upgradeDBFromVersion(jaspVersion); + try + { + _db->close(); + _db->load(); + _db->upgradeDBFromVersion(jaspVersion); - _dataSet = new DataSet(0); // Setting 0 for "do nothing" because otherwise we can't pass on jaspVersion - _dataSet->dbLoad(1, [](float p) {}, jaspVersion); + std::unique_ptr loadedDataSet(new DataSet(0)); // Setting 0 for "do nothing" because otherwise we can't pass on jaspVersion + loadedDataSet->dbLoad(1, [](float p) {}, jaspVersion); - ColumnEncoder::columnEncoder()->setCurrentNames(_dataSet->getColumnTypesMap()); + _dataSet = loadedDataSet.release(); + ColumnEncoder::columnEncoder()->setCurrentNames(_dataSet->getColumnTypesMap()); + endResetModel(); + } + catch (...) + { + _dataSet = new DataSet(); + endResetModel(); + throw; + } } QVariantList DataSetProvider::_getDoubleList(Column * column) const diff --git a/QMLComponents/datasetprovider.h b/QMLComponents/datasetprovider.h index 9c3539338b..52df51fe71 100644 --- a/QMLComponents/datasetprovider.h +++ b/QMLComponents/datasetprovider.h @@ -41,6 +41,7 @@ class DataSetProvider : public QAbstractTableModel, public VariableInfoProvider QVariant data( const QModelIndex & index, int role = Qt::DisplayRole) const override; void loadDataSet(const std::map & dataSet, int threshold = 10, bool orderLabelsByValue = true); + void closeDatabase(); void loadDatabase(const Version & jaspVersion); QVariant provideInfo(VariableInfo::InfoType info, const QString& colName = "", int row = 0) const override; @@ -59,6 +60,7 @@ class DataSetProvider : public QAbstractTableModel, public VariableInfoProvider DatabaseInterface * _db = nullptr; DataSet * _dataSet = nullptr; + bool _inMemory = true; }; diff --git a/R-Interface/jasprcpp.cpp b/R-Interface/jasprcpp.cpp index db5e889ce8..37799b5a5a 100644 --- a/R-Interface/jasprcpp.cpp +++ b/R-Interface/jasprcpp.cpp @@ -80,9 +80,10 @@ int __parseEval(const std::string & line, SEXP & ans) //#ifdef PRINT_ENGINE_MESSAGES //jaspRCPP_logString("parseEval: " + line + "\n"); //#endif + ans = R_NilValue; ParseStatus status; SEXP cmdSexp, cmdexpr = R_NilValue; - int i, errorOccurred; + int i, errorOccurred, rc = 0; PROTECT(cmdSexp = Rf_allocVector(STRSXP, 1)); SET_STRING_ELT(cmdSexp, 0, Rf_mkChar(line.c_str())); @@ -94,17 +95,21 @@ int __parseEval(const std::string & line, SEXP & ans) for(i = 0; i < Rf_length(cmdexpr); i++){ ans = R_tryEval(VECTOR_ELT(cmdexpr, i), Rcpp::Environment::global_env(), &errorOccurred); if (errorOccurred) { - UNPROTECT(2); - return 1; + rc = 1; + break; } } } - return 0; + else + rc = 1; + + UNPROTECT(2); + return rc; } SEXP _parseEval(const std::string &line) { - SEXP ans; + SEXP ans = R_NilValue; int rc = __parseEval(line, ans); if (rc != 0) { throw std::runtime_error(std::string("Error evaluating: ") + line); @@ -239,8 +244,11 @@ void STDCALL jaspRCPP_init(const char* buildYear, const char* version, RBridgeCa jaspRCPP_parseEvalQNT("library(methods)"); - jaspRCPP_logString("Loading friendly R functions for computed columns and filters."); - jaspRCPP_parseEvalQNT(initFriendlyFunctionsRCode, false, false); + if (initFriendlyFunctionsRCode && initFriendlyFunctionsRCode[0] != '\0') + { + jaspRCPP_logString("Loading friendly R functions for computed columns and filters."); + jaspRCPP_parseEvalQNT(initFriendlyFunctionsRCode, false, false); + } _R_HOME = jaspRCPP_parseEvalStringReturn("R.home('')"); jaspRCPP_logString("jaspRCPP_init is done, R_HOME is: " + _R_HOME + "\n"); @@ -270,22 +278,22 @@ void STDCALL jaspRCPP_init_jaspBase() auto rEnvironment = Rcpp::Environment::global_env(); - rEnvironment[".logString"] = Rcpp::XPtr( & _logFuncDef); - rEnvironment[".createColumn"] = Rcpp::XPtr( & _createColumnFuncDef); - rEnvironment[".deleteColumn"] = Rcpp::XPtr( & _deleteColumnFuncDef); - rEnvironment[".getColumnType"] = Rcpp::XPtr( & _getColumnTypeFuncDef); - rEnvironment[".getColumnExists"] = Rcpp::XPtr( & _getColumnExistsFuncDef); - rEnvironment[".getColumnAnalysisId"] = Rcpp::XPtr( & _getColumnAnIdFuncDef); - rEnvironment[".getColumnOriginalIndex"] = Rcpp::XPtr( & _getColumnIndexFuncDef); - rEnvironment[".sendToDesktopFunction"] = Rcpp::XPtr( & _sendToDesktop); - rEnvironment[".pollMessagesFunction"] = Rcpp::XPtr( & _pollMessagesFunction); - rEnvironment[".setColumnDataAsScalePtr"] = Rcpp::XPtr( & _setColumnDataAsScale); - rEnvironment[".setColumnDataAsOrdinalPtr"] = Rcpp::XPtr( & _setColumnDataAsOrdinal); - rEnvironment[".setColumnDataAsNominalPtr"] = Rcpp::XPtr( & _setColumnDataAsOrdinal); - rEnvironment[".shouldEncodeColName"] = Rcpp::XPtr(& _shouldEncodeColumnName); - rEnvironment[".shouldDecodeColName"] = Rcpp::XPtr(& _shouldDecodeColumnName); - rEnvironment[".encodeColName"] = Rcpp::XPtr( & _encodeColumnName); - rEnvironment[".decodeColName"] = Rcpp::XPtr( & _decodeColumnName); + rEnvironment[".logString"] = Rcpp::XPtr( & _logFuncDef, false); + rEnvironment[".createColumn"] = Rcpp::XPtr( & _createColumnFuncDef, false); + rEnvironment[".deleteColumn"] = Rcpp::XPtr( & _deleteColumnFuncDef, false); + rEnvironment[".getColumnType"] = Rcpp::XPtr( & _getColumnTypeFuncDef, false); + rEnvironment[".getColumnExists"] = Rcpp::XPtr( & _getColumnExistsFuncDef, false); + rEnvironment[".getColumnAnalysisId"] = Rcpp::XPtr( & _getColumnAnIdFuncDef, false); + rEnvironment[".getColumnOriginalIndex"] = Rcpp::XPtr( & _getColumnIndexFuncDef, false); + rEnvironment[".sendToDesktopFunction"] = Rcpp::XPtr( & _sendToDesktop, false); + rEnvironment[".pollMessagesFunction"] = Rcpp::XPtr( & _pollMessagesFunction, false); + rEnvironment[".setColumnDataAsScalePtr"] = Rcpp::XPtr( & _setColumnDataAsScale, false); + rEnvironment[".setColumnDataAsOrdinalPtr"] = Rcpp::XPtr( & _setColumnDataAsOrdinal, false); + rEnvironment[".setColumnDataAsNominalPtr"] = Rcpp::XPtr( & _setColumnDataAsNominal, false); + rEnvironment[".shouldEncodeColName"] = Rcpp::XPtr(& _shouldEncodeColumnName, false); + rEnvironment[".shouldDecodeColName"] = Rcpp::XPtr(& _shouldDecodeColumnName, false); + rEnvironment[".encodeColName"] = Rcpp::XPtr( & _encodeColumnName, false); + rEnvironment[".decodeColName"] = Rcpp::XPtr( & _decodeColumnName, false); //Pass a whole bunch of pointers to jaspBase jaspRCPP_parseEvalQNT("jaspBase:::setColumnFuncs( .setColumnDataAsScalePtr, .setColumnDataAsOrdinalPtr, .setColumnDataAsNominalPtr, .getColumnType, .getColumnAnalysisId, .getColumnOriginalIndex, .createColumn, .deleteColumn, .getColumnExists, .encodeColName, .decodeColName, .shouldEncodeColName, .shouldDecodeColName)"); @@ -1240,12 +1248,53 @@ std::string __sinkMe(const std::string code) return "sink(.outputSink);\n" + code; //default type = c('message', 'output') anyway } +class SinkGuard +{ +public: + SinkGuard() + { + _parseEvalQNT(__sinkMe()); + } + + ~SinkGuard() + { + close(); + } + + void close() + { + if (!_active) + return; + + try + { + SEXP ignored = R_NilValue; + int rc = __parseEval("sink();", ignored); + if (rc != 0) + jaspRCPP_logString("SinkGuard failed to close the R output sink.\n"); + } + catch (const std::exception & exception) + { + jaspRCPP_logString(std::string("SinkGuard failed to close the R output sink: ") + exception.what() + "\n"); + } + catch (...) + { + jaspRCPP_logString("SinkGuard failed to close the R output sink with an unknown exception.\n"); + } + + _active = false; + } + +private: + bool _active = true; +}; + void jaspRCPP_setWorkingDirectory() { std::string root = requestTempRootNameCB(); std::string code = "setwd(\"" + root + "\");"; - _parseEvalQNT(__sinkMe(code)); - _parseEvalQNT("sink();"); //Back to normal! + SinkGuard sinkGuard; + _parseEvalQNT(code); } void jaspRCPP_parseEvalQNT(const std::string & code, bool setWd, bool preface) @@ -1256,10 +1305,9 @@ void jaspRCPP_parseEvalQNT(const std::string & code, bool setWd, bool preface) if(preface) jaspRCPP_parseEvalPreface(code); - _parseEvalQNT(__sinkMe()); + SinkGuard sinkGuard; _parseEvalQNT(code); jaspRCPP_logString("\n"); - _parseEvalQNT("sink();"); //Back to normal! } std::string jaspRCPP_parseEvalStringReturn(const std::string & code, bool setWd, bool preface) @@ -1278,12 +1326,12 @@ SEXP jaspRCPP_parseEval(const std::string & code, bool setWd, bool preface) if(preface) jaspRCPP_parseEvalPreface(code); - _parseEvalQNT(__sinkMe()); - SEXP returnthis = _parseEval(code); //Not throwing is nice actually! Well, unless you want to hear about missing modules etc... + SinkGuard sinkGuard; + SEXP returnthis = PROTECT(_parseEval(code)); // Keep the result alive while resetting the sink below. jaspRCPP_logString("\n"); + sinkGuard.close(); - _parseEvalQNT("sink();"); //back to normal! - + UNPROTECT(1); return returnthis; } @@ -1411,4 +1459,3 @@ SEXP jaspRCPP_CreateCaptureConnection() UNPROTECT(1); return rc; } - diff --git a/SyntaxInterface/CMakeLists.txt b/SyntaxInterface/CMakeLists.txt index fa13337521..756a95fd74 100644 --- a/SyntaxInterface/CMakeLists.txt +++ b/SyntaxInterface/CMakeLists.txt @@ -43,7 +43,7 @@ endif() target_compile_definitions( SyntaxInterface - PUBLIC + PRIVATE SYNTAX_INTERFACE_LIBRARY JASP_R_INTERFACE_LIBRARY ) @@ -51,7 +51,7 @@ target_compile_definitions( if(USE_QT_STATIC_LIBS) target_compile_definitions( SyntaxInterface - PUBLIC + PRIVATE USE_QT_STATIC_LIBS ) endif() diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index fddb50e662..82aa02a711 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -40,6 +42,10 @@ #include "utilities/appdirs.h" #include "modules/dynamicmodule.h" #include "archivereader.h" +#include "databaseinterface.h" + +#include +#include #include #ifdef USE_QT_STATIC_LIBS @@ -51,11 +57,17 @@ Q_IMPORT_PLUGIN(JASP_ControlsPlugin) #define STRINGIZE(x) _STRINGIZE(x) static bool gl_initialized = false; +static bool gl_initializedDbInMemory = false; +static bool gl_rBridgeInitialized = false; +static bool gl_jaspBaseInitialized = false; static QGuiApplication * gl_application = nullptr; static QQmlEngine * gl_qmlEngine = nullptr; static DataBridge * gl_dataBridge = nullptr; static ColumnEncoder * gl_extraEncodings = nullptr; static QMap > gl_qmlFormMap; +static int gl_applicationArgc = 0; +static std::vector gl_applicationArgvStorage; +static std::vector gl_applicationArgv; static bool gl_verbose = #ifdef JASP_DEBUG @@ -73,12 +85,154 @@ static std::string gl_param_resultFont = "freesans,sans-serif"; #endif -extern "C" { -void STDCALL syntaxBridgeCleanup() +static bool readJaspJsonEntry(Json::Value & root, const char * filePath, const char * entry, std::string * error = nullptr) +{ + try + { + if (!filePath || std::string(filePath).empty()) + { + if (error) + *error = "Cannot read from an empty JASP archive path."; + return false; + } + + ArchiveReader reader(filePath, entry); + int errorCode = 0; + std::string json = reader.readAllData(sizeof(char), errorCode); + if (errorCode != 0) + { + if (error) + *error = std::string("Could not read entry ") + entry + " from JASP archive " + filePath + "."; + else + Log::log() << "Could not read JASP archive entry." << std::endl; + return false; + } + + Json::Reader parser; + if (!parser.parse(json, root)) + { + if (error) + *error = std::string("Could not parse entry ") + entry + " from JASP archive " + filePath + "."; + else + Log::log() << "Could not parse JASP archive entry." << std::endl; + return false; + } + return true; + } + catch (const std::exception & exception) + { + if (error) + *error = std::string("Could not read entry ") + entry + " from JASP archive " + (filePath ? filePath : "") + ": " + exception.what(); + else + Log::log() << "Could not read JASP archive entry." << std::endl; + return false; + } +} + +static const char* statusResult(Json::Value status) +{ + static std::string result; + result = status.toStyledString(); + return result.c_str(); +} + +static Json::Value statusBase(const char * operation) +{ + Json::Value status(Json::objectValue); + status["operation"] = operation; + status["ok"] = false; + return status; +} + +static const char* statusError(Json::Value status, const std::string & error) +{ + status["ok"] = false; + status["error"] = error; + Log::log() << error << std::endl; + return statusResult(status); +} + +static Json::Value analysisOptionsStatus(const char * filePath, int analysisNr) +{ + Json::Value status = statusBase("syntaxBridgeAnalysisOptionsFromJaspFile"); + status["analysisNr"] = analysisNr; + + Json::Value analysesJson; + std::string error; + if (!readJaspJsonEntry(analysesJson, filePath, "analyses.json", &error)) + { + status["failure"] = "read"; + status["error"] = error; + return status; + } + + const Json::Value & analyses = analysesJson.isArray() ? analysesJson : analysesJson["analyses"]; + if (!analyses.isArray()) + { + status["failure"] = "schema"; + status["error"] = std::string("JASP archive analyses.json does not contain an analyses array."); + return status; + } + + status["analysisCount"] = static_cast(analyses.size()); + if (analysisNr < 0 || analysisNr >= int(analyses.size())) + { + status["failure"] = "index"; + status["error"] = std::string("Could not find analysis ") + std::to_string(analysisNr) + " in JASP archive."; + return status; + } + + const Json::Value & options = analyses[analysisNr]["options"]; + if (options.isNull()) + { + status["failure"] = "schema"; + status["error"] = std::string("Analysis ") + std::to_string(analysisNr) + " does not contain options."; + return status; + } + + status["ok"] = true; + status["options"] = options; + return status; +} + +static void clearRequestedDataState() +{ + rbridge_setWantedCols(ColumnEncoder::colsPlusTypes()); + + ColumnEncoder::colTypeMap noColumns; + ColumnEncoder::columnEncoder()->setCurrentNames(noColumns); + + if (gl_extraEncodings) + gl_extraEncodings->setCurrentNames(noColumns); +} + +static void clearDataBridgeState() +{ + clearRequestedDataState(); + rbridge_clearDataBridge(); + + if (gl_dataBridge) + { + delete gl_dataBridge; + gl_dataBridge = nullptr; + } +} + +static void createDataBridge(bool dbInMemory) +{ + gl_dataBridge = new DataBridge(ProcessInfo::currentPID(), dbInMemory); + rbridge_setDataBridge(gl_dataBridge); + gl_initializedDbInMemory = dbInMemory; +} + +static void clearQmlFormCache() { for (auto value : gl_qmlFormMap.values()) deleteQuickItem(value.second); + if (gl_application) + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + gl_qmlFormMap.clear(); if (gl_qmlEngine) @@ -88,6 +242,113 @@ void STDCALL syntaxBridgeCleanup() } } +static void refreshQmlDataSetInfoContext() +{ + if (gl_qmlEngine) + gl_qmlEngine->rootContext()->setContextProperty("dataSetInfo", VariableInfo::info()); +} + +static DataSetProvider* resetDataProvider(bool dbInMemory, bool resetDataSet) +{ + bool providerWillBeRecreated = gl_initialized && gl_initializedDbInMemory != dbInMemory; + if (providerWillBeRecreated) + clearQmlFormCache(); + + DataSetProvider * provider = DataSetProvider::getProvider(dbInMemory, resetDataSet, gl_application); + gl_initializedDbInMemory = dbInMemory; + refreshQmlDataSetInfoContext(); + return provider; +} + +static bool recreateCleanDataBridgeState(bool dbInMemory) +{ + try + { + clearDataBridgeState(); + DataSetProvider::getProvider(!dbInMemory, true, gl_application); + resetDataProvider(dbInMemory, true); + createDataBridge(dbInMemory); + return gl_dataBridge != nullptr; + } + catch (const std::exception & exception) + { + Log::log() << "Could not restore SyntaxInterface native dataset state after failed JASP archive load: " << exception.what() << std::endl; + } + catch (...) + { + Log::log() << "Could not restore SyntaxInterface native dataset state after failed JASP archive load." << std::endl; + } + + return false; +} + +extern "C" { +void STDCALL syntaxBridgeClearQmlState() +{ + clearQmlFormCache(); +} + +void STDCALL syntaxBridgeClearDataSetState() +{ + clearDataBridgeState(); + + if (gl_initialized) + { + resetDataProvider(gl_initializedDbInMemory, true); + createDataBridge(gl_initializedDbInMemory); + } +} + +void STDCALL syntaxBridgeClearNativeState() +{ + syntaxBridgeClearQmlState(); + syntaxBridgeClearDataSetState(); +} + +void STDCALL syntaxBridgeCleanup() +{ + syntaxBridgeClearQmlState(); +} + +void STDCALL syntaxBridgeShutdown() +{ + syntaxBridgeClearQmlState(); + clearDataBridgeState(); + + if (gl_extraEncodings) + { + delete gl_extraEncodings; + gl_extraEncodings = nullptr; + } + + if (gl_initialized) + { + DataSetProvider * provider = DataSetProvider::getProvider(gl_initializedDbInMemory, false, gl_application); + delete provider; + } + + if (gl_qmlEngine) + { + delete gl_qmlEngine; + gl_qmlEngine = nullptr; + } + + if (gl_application) + { + gl_application->processEvents(); + delete gl_application; + gl_application = nullptr; + } + + gl_applicationArgc = 0; + gl_applicationArgv.clear(); + gl_applicationArgvStorage.clear(); + gl_initialized = false; + gl_initializedDbInMemory = false; + gl_rBridgeInitialized = false; + gl_jaspBaseInitialized = false; +} + void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataSet* syntaxBridgeDataSet, bool dbInMemory, int threshold, bool orderLabelsByValue) { if (!init(dbInMemory)) @@ -96,7 +357,15 @@ void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataSet* syntaxBridgeData return; } - DataSetProvider* provider = DataSetProvider::getProvider(dbInMemory); + DataSetProvider* provider = nullptr; + if (gl_initializedDbInMemory != dbInMemory) + { + clearDataBridgeState(); + provider = resetDataProvider(dbInMemory, true); + createDataBridge(dbInMemory); + } + else + provider = DataSetProvider::getProvider(dbInMemory); std::map dataSet; @@ -112,6 +381,82 @@ void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataSet* syntaxBridgeData provider->loadDataSet(dataSet, threshold, orderLabelsByValue); } +void STDCALL syntaxBridgeLoadDataSetFromJaspFile(const char * filePath, bool dbInMemory) +{ + syntaxBridgeLoadDataSetFromJaspFileStatus(filePath, dbInMemory); +} + +const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * filePath, bool dbInMemory) +{ + Json::Value status = statusBase("syntaxBridgeLoadDataSetFromJaspFile"); + status["dbInMemoryRequested"] = dbInMemory; + status["dbInMemoryUsed"] = false; + + if (!filePath || std::string(filePath).empty()) + return statusError(status, "Cannot load dataset from an empty JASP archive path."); + + if (dbInMemory) + status["warning"] = "dbInMemory=TRUE is ignored for .jasp archives; SyntaxInterface loads archive databases through file-backed internal.sqlite."; + + if (!init(false)) + { + return statusError(status, "Error during initialization."); + } + + bool nativeStateMutated = false; + + try + { + Json::Value manifest; + std::string manifestError; + if (!readJaspJsonEntry(manifest, filePath, "manifest.json", &manifestError)) + return statusError(status, manifestError); + + std::string jaspVersionStr = manifest.get("jaspVersion", "").asString(); + std::string archiveVersionStr = manifest.get("jaspArchiveVersion", "").asString(); + if (archiveVersionStr.empty()) + return statusError(status, "JASP archive manifest is missing jaspArchiveVersion."); + if (jaspVersionStr.empty()) + return statusError(status, "JASP archive manifest is missing jaspVersion."); + + status["jaspArchiveVersion"] = archiveVersionStr; + status["jaspVersion"] = jaspVersionStr; + + Version jaspVersion(jaspVersionStr); + + // Keep SyntaxInterface below Desktop's DataSetPackage/UI ownership while + // mirroring the archive import steps that matter for backend replay: + // extract internal.sqlite, upgrade it for the saved JASP version, then + // expose it through the bridge-owned DataBridge. + clearDataBridgeState(); + nativeStateMutated = true; + DataSetProvider * provider = resetDataProvider(false, false); + provider->closeDatabase(); + ArchiveReader(filePath, DatabaseInterface::singleton()->dbFile(true)).writeEntryToTempFiles([](float) {}); + provider->loadDatabase(jaspVersion); + status["databaseUpgraded"] = true; + createDataBridge(false); + + DataSet * dataSet = gl_dataBridge ? gl_dataBridge->provideAndUpdateDataSet() : nullptr; + if (!dataSet) + { + status["nativeStateRestored"] = recreateCleanDataBridgeState(false); + return statusError(status, std::string("Could not load dataset from JASP archive ") + filePath + ": no dataset was provided by the bridge."); + } + + status["ok"] = true; + status["columnCount"] = static_cast(dataSet->columnCount()); + status["rowCount"] = static_cast(dataSet->rowCount()); + return statusResult(status); + } + catch (const std::exception & exception) + { + if (nativeStateMutated) + status["nativeStateRestored"] = recreateCleanDataBridgeState(false); + return statusError(status, std::string("Could not load dataset from JASP archive ") + filePath + ": " + exception.what()); + } +} + const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData) { if (!init()) @@ -155,6 +500,31 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, c return result.c_str(); } +const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr) +{ + static std::string result; + result = ""; + + Json::Value status = analysisOptionsStatus(filePath, analysisNr); + if (!status["ok"].asBool()) + { + if (status.isMember("error")) + Log::log() << status["error"].asString() << std::endl; + return result.c_str(); + } + + result = status["options"].toStyledString(); + return result.c_str(); +} + +const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFileStatus(const char * filePath, int analysisNr) +{ + Json::Value status = analysisOptionsStatus(filePath, analysisNr); + if (!status["ok"].asBool() && status.isMember("error")) + Log::log() << status["error"].asString() << std::endl; + return statusResult(status); +} + const char* STDCALL syntaxBridgeGenerateModuleWrappers(const char* modulePath) { @@ -263,71 +633,28 @@ const char* STDCALL syntaxBridgeParseDescription(const char* modulePath) return result.c_str(); } -void STDCALL syntaxBridgeLoadDataSetFromJaspFile(const char * filePath, bool dbInMemory) +const char* STDCALL syntaxBridgeGetVariableNames() { - if (!init(dbInMemory)) - { - Log::log() << "Error during initialization" << std::endl; - return; - } - - ArchiveReader(filePath, DatabaseInterface::singleton()->dbFile(true)).writeEntryToTempFiles([](float p){}); - ManifestInfo info = ArchiveReader::readManifest(filePath); - - DataSetProvider* provider = DataSetProvider::getProvider(dbInMemory, false); - - provider->loadDatabase(info.jaspVersion); -} + static std::string result; -const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr) -{ if (!init()) { Log::log() << "Error during initialization" << std::endl; - return ""; - } - - static std::string result; - - result = ""; - Json::Value analysesData; - - if (ArchiveReader::parseJsonEntry(analysesData, filePath, "analyses.json", false)) - { - Json::Value analysesDataList = analysesData.get("analyses", analysesData); - if (analysisNr < analysesDataList.size()) - result = analysesDataList[analysisNr]["options"].toStyledString(); - else - Log::log() << "Analyis number is higher than the number of analyses (" << analysesDataList.size() << ") in the JASP file" << std::endl; + result = ""; + return result.c_str(); } - else - Log::log() << "Fail to open or read the JASP file " << filePath << std::endl; - - return result.c_str(); -} - -const char* STDCALL syntaxBridgeGetVariableNames() -{ - DataSetProvider* provider = DataSetProvider::getProvider(false, false); - if (!provider) - return ""; - - static std::string result; + size_t numCols = 0; + const char ** columnNames = rbridge_allColumnNames(numCols, false); - QStringList names = provider->provideInfo(VariableInfo::VariableNames).toStringList(); Json::Value jsonNames(Json::arrayValue); - - for (const QString & name : names) - jsonNames.append(fq(name)); + for (size_t i = 0; i < numCols; ++i) + jsonNames.append(columnNames[i]); result = jsonNames.toStyledString(); - return result.c_str(); } - - } // extern "C" @@ -356,6 +683,7 @@ bool init(bool dbInMemory) { if (gl_initialized) return true; gl_initialized = true; + gl_initializedDbInMemory = dbInMemory; if (gl_verbose) { @@ -370,31 +698,19 @@ bool init(bool dbInMemory) Log::log() << "R_HOME: " << fq(rHome) << std::endl; } - int dummyArgc = 1; - char dummyArgv[2]; - dummyArgv[0] = '?'; - dummyArgv[1] = '\0'; - //const char* platformArg = "-platform"; //const char* platformOpt = "minimal"; //"cocoa"; - std::vector arguments = {"JASP"}; //{qmlR, platformArg, platformOpt}; - - - int argc = arguments.size(); - char** argvs = new char*[argc]; - - for (int i = 0; i < argc; i++) - { - argvs[i] = new char[strlen(arguments[i]) + 1]; - memset(argvs[i], '\0', strlen(arguments[i]) + 1); - memcpy(argvs[i], arguments[i], strlen(arguments[i])); - argvs[i][ strlen(arguments[i])] = '\0'; - } + gl_applicationArgvStorage = {"JASP"}; //{qmlR, platformArg, platformOpt}; + gl_applicationArgv.clear(); + for (std::string & argument : gl_applicationArgvStorage) + gl_applicationArgv.push_back(argument.data()); + gl_applicationArgv.push_back(nullptr); + gl_applicationArgc = static_cast(gl_applicationArgvStorage.size()); qputenv("QT_QPA_PLATFORM", "minimal"); - gl_application = new QGuiApplication(argc, argvs); + gl_application = new QGuiApplication(gl_applicationArgc, gl_applicationArgv.data()); gl_qmlEngine = new QQmlEngine(); Dirs::setLocalAppdataDir(AppDirs::appData(false).toStdString()); @@ -404,21 +720,43 @@ bool init(bool dbInMemory) QmlUtils::setupQMLEngine(gl_qmlEngine); QmlUtils::registerQmlModuleTypes(); - gl_dataBridge = new DataBridge(ProcessInfo::currentPID(), dbInMemory); + createDataBridge(dbInMemory); gl_extraEncodings = new ColumnEncoder("JaspExtraOptions_"); rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); - - jaspRCPP_init_jaspBase(); + gl_rBridgeInitialized = true; return true; } +void ensureRBridgeInitialized() +{ + if (gl_rBridgeInitialized) + return; + + rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); + gl_rBridgeInitialized = true; +} + +void ensureJaspBaseInitialized() +{ + if (gl_jaspBaseInitialized) + return; + + // Option parsing and dataset replay do not need jaspBase. Load it only for + // the less common path where QML explicitly asks to evaluate R code. + ensureRBridgeInitialized(); + jaspRCPP_init_jaspBase(); + gl_jaspBaseInitialized = true; +} + void sendRScriptHandler(AnalysisForm* form, QString script, QString controlName, bool whiteListedVersion) { if (gl_verbose) Log::log() << "R Script " << fq(script) << " sent by " << controlName << std::endl; + ensureJaspBaseInitialized(); + bool hasError = false; std::string result = rbridge_evalRCodeWhiteListed(fq(script).c_str(), whiteListedVersion); diff --git a/SyntaxInterface/syntaxbridge_interface.h b/SyntaxInterface/syntaxbridge_interface.h index 0c0d5b54bc..e228746288 100644 --- a/SyntaxInterface/syntaxbridge_interface.h +++ b/SyntaxInterface/syntaxbridge_interface.h @@ -39,30 +39,37 @@ extern "C" { +// Keep these exported structs plain C ABI data. Callers must initialize every +// field explicitly, e.g. with {} in C++ or calloc/memset in C. struct SyntaxBridgeColumn { - char * name = nullptr; - char ** values = nullptr; + char * name; + char ** values; } ; struct SyntaxBridgeDataSet { - char * name = nullptr; - int rowCount = 0; - int columnCount = 0; - SyntaxBridgeColumn * columns = nullptr; + char * name; + int rowCount; + int columnCount; + SyntaxBridgeColumn * columns; }; SYNTAX_INTERFACE void STDCALL syntaxBridgeCleanup(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeShutdown(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeClearQmlState(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeClearDataSetState(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeClearNativeState(); SYNTAX_INTERFACE void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataSet* dataset, bool dbInMemory, int threshold, bool orderLabelsByValue); SYNTAX_INTERFACE void STDCALL syntaxBridgeLoadDataSetFromJaspFile(const char * filePath, bool dbInMemory); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * filePath, bool dbInMemory); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char * moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFileStatus(const char * filePath, int analysisNr); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGenerateModuleWrappers(const char* name); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGenerateAnalysisWrapper(const char* modulePath, const char* analysisName); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeParseDescription(const char* modulePath); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGetVariableNames(); - } // extern "C" #endif // SYNTAXBRIDGE_INTERFACE_H