diff --git a/CompileHowto.md b/CompileHowto.md index 11c6b93b..9a125efe 100644 --- a/CompileHowto.md +++ b/CompileHowto.md @@ -58,7 +58,7 @@ sudo make install/strip # to uninstall (not very well tested, please keep that in mind) sudo make uninstall # ... or run it from compile directory -bin/hyperiond myconfig.json +bin/hyperiond # webui is located on localhost:8099 ``` @@ -143,5 +143,3 @@ This will install to ``/home/pi/apps/share/hyperion`` ### Integrating hyperion into your system ... ToDo - - diff --git a/assets/webconfig/js/content_effectsconfigurator.js b/assets/webconfig/js/content_effectsconfigurator.js index 6152bc3b..9d7eae61 100644 --- a/assets/webconfig/js/content_effectsconfigurator.js +++ b/assets/webconfig/js/content_effectsconfigurator.js @@ -2,13 +2,13 @@ $(document).ready( function() { performTranslation(); var oldDelList = []; var effectName = ""; - var effects_editor = null; + var effects_editor = null; var effectPy = ""; var testrun; - + if(showOptHelp) createHintH("intro", $.i18n('effectsconfigurator_label_intro'), "intro_effc"); - + function updateDelEffectlist(){ var newDelList = serverInfo.effects; if(newDelList.length != oldDelList.length) @@ -27,25 +27,25 @@ $(document).ready( function() { oldDelList = newDelList; } } - + function triggerTestEffect() { testrun = true; var args = effects_editor.getEditor('root.args'); requestTestEffect(effectName, ":/effects/" + effectPy.slice(1), JSON.stringify(args.getValue())); }; - - + + $("#effectslist").off().on("change", function(event) { if(effects_editor != null) effects_editor.destroy(); - + for(var idx=0; idx /dev/null 2>&1 & echo $!` + PID=`$DAEMON > /dev/null 2>&1 & echo $!` #echo "Saving PID" $PID " to " $PIDFILE if [ -z $PID ]; then printf "%s\n" "Fail" diff --git a/bin/service/hyperion.initctl b/bin/service/hyperion.initctl index 86ef5923..05e52f0c 100644 --- a/bin/service/hyperion.initctl +++ b/bin/service/hyperion.initctl @@ -8,4 +8,4 @@ stop on (runlevel [!2345]) respawn -exec /usr/bin/hyperiond /etc/hyperion/hyperion.config.json +exec /usr/bin/hyperiond diff --git a/bin/service/hyperion.systemd b/bin/service/hyperion.systemd index 538da52c..8d9f1f2c 100644 --- a/bin/service/hyperion.systemd +++ b/bin/service/hyperion.systemd @@ -3,7 +3,7 @@ Description=Hyperion ambient light systemd service After=network.target [Service] -ExecStart=/usr/bin/hyperiond /etc/hyperion/hyperion.config.json +ExecStart=/usr/bin/hyperiond WorkingDirectory=/usr/share/hyperion/bin TimeoutStopSec=5 KillMode=mixed diff --git a/config/hyperion.config.json.commented b/config/hyperion.config.json.commented index 74525844..92e95b83 100644 --- a/config/hyperion.config.json.commented +++ b/config/hyperion.config.json.commented @@ -319,17 +319,18 @@ { "enable" : true, "document_root" : "/path/to/files", - "port" : 8099 + "port" : 8090 }, /// The configuration of the effect engine, contains the following items: - /// * paths : An array with absolute/relative location(s) of directories with effects + /// * paths : An array with absolute location(s) of directories with effects, + /// $ROOT is a keyword which will be replaced with the current rootPath that can be specified on startup from the commandline (defaults to your home directory) /// * disable : An array with effect names that shouldn't be loaded "effects" : { "paths" : [ - "/storage/hyperion/effects", + "$ROOT/custom-effects", "/usr/share/hyperion/effects" ], "disable" : diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default index 47ca888a..b6849c70 100644 --- a/config/hyperion.config.json.default +++ b/config/hyperion.config.json.default @@ -181,12 +181,12 @@ { "enable" : true, "document_root" : "", - "port" : 8099 + "port" : 8090 }, "effects" : { - "paths" : ["../custom-effects"], + "paths" : ["$ROOT/custom-effects"], "disable": [""] }, diff --git a/include/effectengine/EffectEngine.h b/include/effectengine/EffectEngine.h index 67b44647..2f5494a7 100644 --- a/include/effectengine/EffectEngine.h +++ b/include/effectengine/EffectEngine.h @@ -57,7 +57,7 @@ public slots: void allChannelsCleared(); private slots: - void effectFinished(Effect * effect); + void effectFinished(); private: bool loadEffectDefinition(const QString & path, const QString & effectConfigFile, EffectDefinition &effectDefinition); @@ -77,7 +77,7 @@ private: std::list _activeEffects; std::list _availableActiveEffects; - + std::list _effectSchemas; PyThreadState * _mainThreadState; diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h index 2329ac0a..5647c8f1 100644 --- a/include/hyperion/Hyperion.h +++ b/include/hyperion/Hyperion.h @@ -80,7 +80,18 @@ public: /// void freeObjects(bool emitCloseSignal=false); - static Hyperion* initInstance(const QJsonObject& qjsonConfig, const QString configFile); + /// + /// @brief creates a new Hyperion instance, usually called from the Hyperion Daemon + /// @param[in] qjsonConfig The configuration file + /// @param[in] rootPath Root path of all hyperion userdata + /// @return Hyperion instance pointer + /// + static Hyperion* initInstance(const QJsonObject& qjsonConfig, const QString configFile, const QString rootPath); + + /// + /// @brief Get a pointer of this Hyperion instance + /// @return Hyperion instance pointer + /// static Hyperion* getInstance(); /// @@ -189,15 +200,17 @@ public: /// gets the methode how image is maped to leds int getLedMappingType() { return _ledMAppingType; }; - int getConfigVersionId() { return _configVersionId; }; - + /// get the configuration QJsonObject getConfig() { return _qjsonConfig; }; + /// get the root path for all hyperion user data files + QString getRootPath() { return _rootPath; }; + /// unique id per instance QString id; int getLatchTime() const; - + /// forward smoothing config unsigned addSmoothingConfig(int settlingTime_ms, double ledUpdateFrequency_hz=25.0, unsigned updateDelay=0); @@ -297,7 +310,7 @@ public slots: /// @param[in] mode The new video mode /// void setVideoMode(VideoMode mode); - + /// /// Set the grabbing mode /// @param[in] mode The new grabbing mode @@ -375,7 +388,7 @@ private: /// /// @param[in] qjsonConfig The Json configuration /// - Hyperion(const QJsonObject& qjsonConfig, const QString configFile); + Hyperion(const QJsonObject& qjsonConfig, const QString configFile, const QString rootPath); /// The specifiation of the led frame construction and picture integration LedString _ledString; @@ -409,6 +422,9 @@ private: /// the name of config file QString _configFile; + /// root path for all hyperion user data files + QString _rootPath; + /// The timer for handling priority channel timeouts QTimer _timer; QTimer _timerBonjourResolver; @@ -439,8 +455,6 @@ private: int _ledMAppingType; - int _configVersionId; - hyperion::Components _prevCompId; BonjourServiceBrowser _bonjourBrowser; BonjourServiceResolver _bonjourResolver; @@ -461,8 +475,8 @@ private: /// timers to handle severinfo blocking QTimer _fsi_timer; - QTimer _fsi_blockTimer; - + QTimer _fsi_blockTimer; + VideoMode _videoMode; GrabbingMode _grabbingMode; }; diff --git a/include/hyperion/ImageProcessor.h b/include/hyperion/ImageProcessor.h index 6e8aa9c2..935fcaea 100644 --- a/include/hyperion/ImageProcessor.h +++ b/include/hyperion/ImageProcessor.h @@ -19,7 +19,7 @@ /// performed in two steps. First the average color per led-region is computed. Second a /// color-tranform is applied based on a gamma-correction. /// -class ImageProcessor : public QObject +class ImageProcessor : public QObject { Q_OBJECT @@ -44,7 +44,7 @@ public: /// Returns starte of black border detector bool blackBorderDetectorEnabled(); - + /// Returns starte of black border detector int ledMappingType(); @@ -177,10 +177,10 @@ private: delete _imageToLeds; _imageToLeds = new hyperion::ImageToLedsMap(image.width(), image.height(), 0, 0, _ledString.leds()); } - + if(_borderProcessor->enabled() && _borderProcessor->process(image)) { - Debug(Logger::getInstance("BLACKBORDER"), "BORDER SWITCH REQUIRED!!"); + //Debug(Logger::getInstance("BLACKBORDER"), "BORDER SWITCH REQUIRED!!"); const hyperion::BlackBorder border = _borderProcessor->getCurrentBorder(); @@ -198,8 +198,8 @@ private: _imageToLeds = new hyperion::ImageToLedsMap(image.width(), image.height(), border.horizontalSize, border.verticalSize, _ledString.leds()); } - Debug(Logger::getInstance("BLACKBORDER"), "CURRENT BORDER TYPE: unknown=%d hor.size=%d vert.size=%d", - border.unknown, border.horizontalSize, border.verticalSize ); + //Debug(Logger::getInstance("BLACKBORDER"), "CURRENT BORDER TYPE: unknown=%d hor.size=%d vert.size=%d", + // border.unknown, border.horizontalSize, border.verticalSize ); } } diff --git a/include/utils/FileUtils.h b/include/utils/FileUtils.h index 06122a5c..52a7e569 100644 --- a/include/utils/FileUtils.h +++ b/include/utils/FileUtils.h @@ -1,10 +1,64 @@ #pragma once +// qt includes +#include #include +#include + +// util includes +#include "Logger.h" namespace FileUtils { QString getBaseName( QString sourceFile); QString getDirName( QString sourceFile); + /// + /// @brief check if the file exists + /// @param[in] path The file path to check + /// @param[in] log The logger of the caller to print errors + /// @param[in] ignError Ignore errors during file read (no log output) + /// @return true on success else false + /// + bool fileExists(const QString& path, Logger* log, bool ignError=false); + + /// + /// @brief read a file given by path. + /// @param[in] path The file path to read + /// @param[out] data The read data o success + /// @param[in] log The logger of the caller to print errors + /// @param[in] ignError Ignore errors during file read (no log output) + /// @return true on success else false + /// + bool readFile(const QString& path, QString& data, Logger* log, bool ignError=false); + + /// + /// write a file given by path. + /// @param[in] path The file path to read + /// @param[in] data The data to write + /// @param[in] log The logger of the caller to print errors + /// @return true on success else false + /// + bool writeFile(const QString& path, const QByteArray& data, Logger* log); + + /// + /// @brief delete a file by given path + /// @param[in] path The file path to delete + /// @param[in] log The logger of the caller to print errors + /// @return true on success else false + /// + bool removeFile(const QString& path, Logger* log); + + /// + /// @brief Convert a path that may contain special placeholders + /// @param[in] path The path to convert + /// + QString convertPath(const QString path); + + /// + /// @brief resolve the file error and print a message + /// @param[in] file The file which caused the error + /// @param[in] log The logger of the caller + /// + void resolveFileError(const QFile& file, Logger* log); }; diff --git a/include/utils/JsonProcessor.h b/include/utils/JsonProcessor.h index 02096144..1b7d7e7e 100644 --- a/include/utils/JsonProcessor.h +++ b/include/utils/JsonProcessor.h @@ -45,9 +45,10 @@ public: /// Constructor /// /// @param peerAddress provide the Address of the peer - /// @param noListener if true, this instance won't listen for hyperion push events + /// @param log The Logger class of the creator + /// @param noListener if true, this instance won't listen for hyperion push events /// - JsonProcessor(QString peerAddress, bool noListener = false); + JsonProcessor(QString peerAddress, Logger* log, bool noListener = false); ~JsonProcessor(); /// @@ -272,16 +273,4 @@ private: /// @param error String describing the error /// void sendErrorReply(const QString & error, const QString &command="", const int tan=0); - - /// - /// Check if a JSON messag is valid according to a given JSON schema - /// - /// @param message JSON message which need to be checked - /// @param schemaResource Qt Resource identifier with the JSON schema - /// @param errors Output error message - /// @param ignoreRequired ignore the required value in JSON schema - /// - /// @return true if message conforms the given JSON schema - /// - bool checkJson(const QJsonObject & message, const QString &schemaResource, QString & errors, bool ignoreRequired = false); }; diff --git a/include/utils/JsonUtils.h b/include/utils/JsonUtils.h new file mode 100644 index 00000000..6187b226 --- /dev/null +++ b/include/utils/JsonUtils.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include +#include + +namespace JsonUtils{ + /// + /// @brief read a json file and get the parsed result on success + /// @param[in] path The file path to read + /// @param[out] obj Returns the parsed QJsonObject + /// @param[in] log The logger of the caller to print errors + /// @param[in] ignError Ignore errors during file read (no log output) + /// @return true on success else false + /// + bool readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError=false); + + /// + /// @brief read a schema file and resolve $refs + /// @param[in] path The file path to read + /// @param[out] obj Returns the parsed QJsonObject + /// @param[in] log The logger of the caller to print errors + /// @return true on success else false + /// + bool readSchema(const QString& path, QJsonObject& obj, Logger* log); + + /// + /// @brief parse a json QString and get the result on success + /// @param[in] path The file path/name just used for log messages + /// @param[in] data Data to parse + /// @param[out] obj Retuns the parsed QJsonObject + /// @param[in] log The logger of the caller to print errors + /// @return true on success else false + /// + bool parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log); + + /// + /// @brief Validate json data against a schema + /// @param[in] file The path/name of json file just used for log messages + /// @param[in] json The json data + /// @param[in] schemaP The schema path + /// @param[in] log The logger of the caller to print errors + /// @return true on success else false + /// + bool validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log); + + /// + /// @brief Write json data to file + /// @param[in] filenameThe file path to write + /// @param[in] json The json data to write + /// @param[in] log The logger of the caller to print errors + /// @return true on success else false + /// + bool write(const QString& filename, const QJsonObject& json, Logger* log); + + /// + /// @brief resolve schema $ref attribute + /// @param[in] schema the schema to iterate + /// @param[out] obj the resolved object + /// @param[in] log The logger of the caller to print errors + /// @return true on success else false + /// + bool resolveRefs(const QJsonObject& schema, QJsonObject& obj, Logger* log); +}; diff --git a/include/utils/jsonschema/QJsonFactory.h b/include/utils/jsonschema/QJsonFactory.h index fb42ee38..2fcf5882 100644 --- a/include/utils/jsonschema/QJsonFactory.h +++ b/include/utils/jsonschema/QJsonFactory.h @@ -25,7 +25,7 @@ public: // create the validator QJsonSchemaChecker schemaChecker; schemaChecker.setSchema(schemaTree); - + QStringList messages = schemaChecker.getMessages(); if (!schemaChecker.validate(configTree).first) @@ -40,7 +40,7 @@ public: json = configTree; return 0; } - + static QJsonObject readConfig(const QString& path) { QFile file(path); @@ -54,7 +54,7 @@ public: //Allow Comments in Config QString config = QString(file.readAll()); config.remove(QRegularExpression("([^:]?\\/\\/.*)")); - + QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8(), &error); file.close(); @@ -94,7 +94,7 @@ public: QByteArray schema = schemaData.readAll(); QJsonDocument doc = QJsonDocument::fromJson(schema, &error); schemaData.close(); - + if (error.error != QJsonParseError::NoError) { // report to the user the failure and their locations in the document. diff --git a/include/utils/jsonschema/QJsonSchemaChecker.h b/include/utils/jsonschema/QJsonSchemaChecker.h index a0c4569f..895cdaa1 100644 --- a/include/utils/jsonschema/QJsonSchemaChecker.h +++ b/include/utils/jsonschema/QJsonSchemaChecker.h @@ -22,6 +22,8 @@ /// - addtionalProperties /// - minItems /// - maxItems +/// - minLength +/// - maxLength class QJsonSchemaChecker { @@ -39,7 +41,7 @@ public: /// @brief Validate a JSON structure /// @param value The JSON value to check /// @param ignoreRequired Ignore the "required" keyword in hyperion schema. Default is false - /// @return The first boolean is true when the arguments is valid according to the schema. The second is true when the schema contains no errors + /// @return The first boolean is true when the arguments is valid according to the schema. The second is true when the schema contains no errors /// @return TODO: Check the Schema in SetSchema() function and remove the QPair result /// QPair validate(const QJsonObject & value, bool ignoreRequired = false); @@ -84,7 +86,7 @@ private: /// @param[in] schema The specified type (as json-value) /// void checkType(const QJsonValue & value, const QJsonValue & schema, const QJsonValue & defaultValue); - + /// /// Checks is required properties of an json-object exist and if all properties are of the /// correct format. If this is not the case _error is set to true and an error-message is added @@ -127,7 +129,7 @@ private: /// Checks if the given value is hugher than the specified value. If this is the /// case _error is set to true and an error-message is added to the message-queue. /// - /// @param value The given value + /// @param value The given value /// @param schema The minimum size specification (as json-value) /// void checkMinLength(const QJsonValue & value, const QJsonValue & schema, const QJsonValue & defaultValue); @@ -136,7 +138,7 @@ private: /// Checks if the given value is smaller than the specified value. If this is the /// case _error is set to true and an error-message is added to the message-queue. /// - /// @param value The given value + /// @param value The given value /// @param schema The maximum size specification (as json-value) /// void checkMaxLength(const QJsonValue & value, const QJsonValue & schema, const QJsonValue & defaultValue); diff --git a/libsrc/effectengine/Effect.cpp b/libsrc/effectengine/Effect.cpp index 15fbbd00..a29dd951 100644 --- a/libsrc/effectengine/Effect.cpp +++ b/libsrc/effectengine/Effect.cpp @@ -78,9 +78,8 @@ void Effect::registerHyperionExtensionModule() PyImport_AppendInittab("hyperion", &PyInit_hyperion); } -Effect::Effect(PyThreadState * mainThreadState, int priority, int timeout, const QString & script, const QString & name, const QJsonObject & args, const QString & origin, unsigned smoothCfg) +Effect::Effect(int priority, int timeout, const QString & script, const QString & name, const QJsonObject & args, const QString & origin, unsigned smoothCfg) : QThread() - , _mainThreadState(mainThreadState) , _priority(priority) , _timeout(timeout) , _script(script) @@ -89,7 +88,6 @@ Effect::Effect(PyThreadState * mainThreadState, int priority, int timeout, const , _args(args) , _endTime(-1) , _interpreterThreadState(nullptr) - , _abortRequested(false) , _imageProcessor(ImageProcessorFactory::getInstance().newImageProcessor()) , _colors() , _origin(origin) @@ -99,15 +97,15 @@ Effect::Effect(PyThreadState * mainThreadState, int priority, int timeout, const _colors.resize(_imageProcessor->getLedCount()); _colors.fill(ColorRgb::BLACK); + _log = Logger::getInstance("EFFECTENGINE"); + // disable the black border detector for effects _imageProcessor->enableBlackBorderDetector(false); - + // init effect image for image based effects, size is based on led layout _image.fill(Qt::black); _painter = new QPainter(&_image); - // connect the finished signal - connect(this, SIGNAL(finished()), this, SLOT(effectFinished())); Q_INIT_RESOURCE(EffectEngine); } @@ -120,7 +118,7 @@ Effect::~Effect() void Effect::run() { // switch to the main thread state and acquire the GIL - PyEval_RestoreThread(_mainThreadState); + PyEval_AcquireLock(); // Initialize a new thread state _interpreterThreadState = Py_NewInterpreter(); @@ -158,13 +156,109 @@ void Effect::run() } else { - Error(Logger::getInstance("EFFECTENGINE"), "Unable to open script file %s.", QSTRING_CSTR(_script)); + Error(_log, "Unable to open script file %s.", QSTRING_CSTR(_script)); } file.close(); if (!python_code.isEmpty()) { - PyRun_SimpleString(python_code.constData()); + PyObject *main_module = PyImport_ImportModule("__main__"); // New Reference + PyObject *main_dict = PyModule_GetDict(main_module); // Borrowed reference + Py_INCREF(main_dict); // Incref "main_dict" to use it in PyRun_String(), because PyModule_GetDict() has decref "main_dict" + Py_DECREF(main_module); // // release "main_module" when done + PyObject *result = PyRun_String(python_code.constData(), Py_file_input, main_dict, main_dict); // New Reference + + if (!result) + { + if (PyErr_Occurred()) // Nothing needs to be done for a borrowed reference + { + Error(_log,"###### PYTHON EXCEPTION ######"); + Error(_log,"## In effect '%s'", QSTRING_CSTR(_name)); + /* Objects all initialized to NULL for Py_XDECREF */ + PyObject *errorType = NULL, *errorValue = NULL, *errorTraceback = NULL; + + PyErr_Fetch(&errorType, &errorValue, &errorTraceback); // New Reference or NULL + PyErr_NormalizeException(&errorType, &errorValue, &errorTraceback); + + // Extract exception message from "errorValue" + if(errorValue) + { + QString message; + if(PyObject_HasAttrString(errorValue, "__class__")) + { + PyObject *classPtr = PyObject_GetAttrString(errorValue, "__class__"); // New Reference + PyObject *class_name = NULL; /* Object "class_name" initialized to NULL for Py_XDECREF */ + class_name = PyObject_GetAttrString(classPtr, "__name__"); // New Reference or NULL + + if(class_name && PyString_Check(class_name)) + message.append(PyString_AsString(class_name)); + + Py_DECREF(classPtr); // release "classPtr" when done + Py_XDECREF(class_name); // Use Py_XDECREF() to ignore NULL references + } + + // Object "class_name" initialized to NULL for Py_XDECREF + PyObject *valueString = NULL; + valueString = PyObject_Str(errorValue); // New Reference or NULL + + if(valueString && PyString_Check(valueString)) + { + if(!message.isEmpty()) + message.append(": "); + + message.append(PyString_AsString(valueString)); + } + Py_XDECREF(valueString); // Use Py_XDECREF() to ignore NULL references + + Error(_log, "## %s", QSTRING_CSTR(message)); + } + + // Extract exception message from "errorTraceback" + if(errorTraceback) + { + // Object "tracebackList" initialized to NULL for Py_XDECREF + PyObject *tracebackModule = NULL, *methodName = NULL, *tracebackList = NULL; + QString tracebackMsg; + + tracebackModule = PyImport_ImportModule("traceback"); // New Reference or NULL + methodName = PyString_FromString("format_exception"); // New Reference or NULL + tracebackList = PyObject_CallMethodObjArgs(tracebackModule, methodName, errorType, errorValue, errorTraceback, NULL); // New Reference or NULL + + if(tracebackList) + { + PyObject* iterator = PyObject_GetIter(tracebackList); // New Reference + + PyObject* item; + while( (item = PyIter_Next(iterator)) ) // New Reference + { + Error(_log, "## %s",QSTRING_CSTR(QString(PyString_AsString(item)).trimmed())); + Py_DECREF(item); // release "item" when done + } + Py_DECREF(iterator); // release "iterator" when done + } + + // Use Py_XDECREF() to ignore NULL references + Py_XDECREF(tracebackModule); + Py_XDECREF(methodName); + Py_XDECREF(tracebackList); + + // Give the exception back to python and print it to stderr in case anyone else wants it. + Py_XINCREF(errorType); + Py_XINCREF(errorValue); + Py_XINCREF(errorTraceback); + + PyErr_Restore(errorType, errorValue, errorTraceback); + //PyErr_PrintEx(0); // Remove this line to switch off stderr output + } + Error(_log,"###### EXCEPTION END ######"); + } + } + else + { + Py_DECREF(result); // release "result" when done + } + + Py_DECREF(main_dict); // release "main_dict" when done } // Clean up the thread state @@ -173,28 +267,8 @@ void Effect::run() PyEval_ReleaseLock(); } -int Effect::getPriority() const -{ - return _priority; -} - -bool Effect::isAbortRequested() const -{ - return _abortRequested; -} - -void Effect::abort() -{ - _abortRequested = true; -} - -void Effect::effectFinished() -{ - emit effectFinished(this); -} - PyObject *Effect::json2python(const QJsonValue &jsonData) const -{ +{ switch (jsonData.type()) { case QJsonValue::Null: @@ -251,7 +325,7 @@ PyObject* Effect::wrapSetColor(PyObject *self, PyObject *args) Effect * effect = getEffect(); // check if we have aborted already - if (effect->_abortRequested) + if (effect->isInterruptionRequested()) { return Py_BuildValue(""); } @@ -333,7 +407,7 @@ PyObject* Effect::wrapSetImage(PyObject *self, PyObject *args) Effect * effect = getEffect(); // check if we have aborted already - if (effect->_abortRequested) + if (effect->isInterruptionRequested()) { return Py_BuildValue(""); } @@ -398,10 +472,10 @@ PyObject* Effect::wrapAbort(PyObject *self, PyObject *) // Test if the effect has reached it end time if (effect->_timeout > 0 && QDateTime::currentMSecsSinceEpoch() > effect->_endTime) { - effect->_abortRequested = true; + effect->requestInterruption(); } - return Py_BuildValue("i", effect->_abortRequested ? 1 : 0); + return Py_BuildValue("i", effect->isInterruptionRequested() ? 1 : 0); } @@ -453,7 +527,7 @@ PyObject* Effect::wrapImageShow(PyObject *self, PyObject *args) binaryImage.append((char) qBlue(scanline[j])); } } - + memcpy(image.memptr(), binaryImage.data(), binaryImage.size()); std::vector v = effect->_colors.toStdVector(); effect->_imageProcessor->process(image, v); @@ -513,7 +587,7 @@ PyObject* Effect::wrapImageLinearGradient(PyObject *self, PyObject *args) gradient.setSpread(static_cast(spread)); effect->_painter->fillRect(myQRect, gradient); - + return Py_BuildValue(""); } else @@ -580,7 +654,7 @@ PyObject* Effect::wrapImageConicalGradient(PyObject *self, PyObject *args) } effect->_painter->fillRect(myQRect, gradient); - + return Py_BuildValue(""); } else @@ -616,7 +690,7 @@ PyObject* Effect::wrapImageRadialGradient(PyObject *self, PyObject *args) if ( argCount == 12 && PyArg_ParseTuple(args, "iiiiiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &focalX, &focalY, &focalRadius, &bytearray, &spread) ) { argsOK = true; - } + } if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &bytearray, &spread) ) { argsOK = true; @@ -635,7 +709,7 @@ PyObject* Effect::wrapImageRadialGradient(PyObject *self, PyObject *args) focalY = centerY; focalRadius = radius; } - + if (argsOK) { if (PyByteArray_Check(bytearray)) @@ -661,7 +735,7 @@ PyObject* Effect::wrapImageRadialGradient(PyObject *self, PyObject *args) gradient.setSpread(static_cast(spread)); effect->_painter->fillRect(myQRect, gradient); - + return Py_BuildValue(""); } else @@ -742,7 +816,7 @@ PyObject* Effect::wrapImageDrawPie(PyObject *self, PyObject *args) { Effect * effect = getEffect(); PyObject * bytearray = nullptr; - + QString brush; int argCount = PyTuple_Size(args); int radius, centerX, centerY; @@ -804,7 +878,7 @@ PyObject* Effect::wrapImageDrawPie(PyObject *self, PyObject *args) )); } painter->setBrush(gradient); - + return Py_BuildValue(""); } else @@ -908,7 +982,7 @@ PyObject* Effect::wrapImageDrawLine(PyObject *self, PyObject *args) painter->setPen(newPen); painter->drawLine(startX, startY, endX, endY); painter->setPen(oldPen); - + return Py_BuildValue(""); } return nullptr; @@ -943,7 +1017,7 @@ PyObject* Effect::wrapImageDrawPoint(PyObject *self, PyObject *args) painter->setPen(newPen); painter->drawPoint(x, y); painter->setPen(oldPen); - + return Py_BuildValue(""); } return nullptr; @@ -983,7 +1057,7 @@ PyObject* Effect::wrapImageDrawRect(PyObject *self, PyObject *args) painter->setPen(newPen); painter->drawRect(startX, startY, width, height); painter->setPen(oldPen); - + return Py_BuildValue(""); } return nullptr; @@ -1028,7 +1102,7 @@ PyObject* Effect::wrapImageSave(PyObject *self, PyObject *args) QImage img(effect->_image.copy()); effect->_imageStack.append(img); - return Py_BuildValue("i", effect->_imageStack.size()-1); + return Py_BuildValue("i", effect->_imageStack.size()-1); } PyObject* Effect::wrapImageMinSize(PyObject *self, PyObject *args) @@ -1045,7 +1119,7 @@ PyObject* Effect::wrapImageMinSize(PyObject *self, PyObject *args) if (width_painter; - + effect->_image = effect->_image.scaled(qMax(width,w),qMax(height,h), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); effect->_imageSize = effect->_image.size(); effect->_painter = new QPainter(&(effect->_image)); @@ -1070,10 +1144,10 @@ PyObject* Effect::wrapImageHeight(PyObject *self, PyObject *args) PyObject* Effect::wrapImageCRotate(PyObject *self, PyObject *args) { Effect * effect = getEffect(); - + int argCount = PyTuple_Size(args); int angle; - + if ( argCount == 1 && PyArg_ParseTuple(args, "i", &angle ) ) { angle = qMax(qMin(angle,360),0); @@ -1086,16 +1160,16 @@ PyObject* Effect::wrapImageCRotate(PyObject *self, PyObject *args) PyObject* Effect::wrapImageCOffset(PyObject *self, PyObject *args) { Effect * effect = getEffect(); - + int offsetX = 0; int offsetY = 0; int argCount = PyTuple_Size(args); - + if ( argCount == 2 ) { PyArg_ParseTuple(args, "ii", &offsetX, &offsetY ); } - + effect->_painter->translate(QPoint(offsetX,offsetY)); return Py_BuildValue(""); } @@ -1103,10 +1177,10 @@ PyObject* Effect::wrapImageCOffset(PyObject *self, PyObject *args) PyObject* Effect::wrapImageCShear(PyObject *self, PyObject *args) { Effect * effect = getEffect(); - + int sh,sv; int argCount = PyTuple_Size(args); - + if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &sh, &sv )) { effect->_painter->shear(sh,sv); diff --git a/libsrc/effectengine/Effect.h b/libsrc/effectengine/Effect.h index 2d23355e..ce4aa8b0 100644 --- a/libsrc/effectengine/Effect.h +++ b/libsrc/effectengine/Effect.h @@ -19,36 +19,26 @@ class Effect : public QThread Q_OBJECT public: - Effect(PyThreadState * mainThreadState, int priority, int timeout, const QString & script, const QString & name, const QJsonObject & args = QJsonObject(), const QString & origin="System", unsigned smoothCfg=0); + Effect(int priority, int timeout, const QString & script, const QString & name, const QJsonObject & args = QJsonObject(), const QString & origin="System", unsigned smoothCfg=0); virtual ~Effect(); virtual void run(); - int getPriority() const; - + int getPriority() const { return _priority; }; + QString getScript() const { return _script; } QString getName() const { return _name; } - - int getTimeout() const {return _timeout; } - - QJsonObject getArgs() const { return _args; } - bool isAbortRequested() const; + int getTimeout() const {return _timeout; } + + QJsonObject getArgs() const { return _args; } /// This function registers the extension module in Python static void registerHyperionExtensionModule(); -public slots: - void abort(); - signals: - void effectFinished(Effect * effect); - void setColors(int priority, const std::vector &ledColors, const int timeout_ms, bool clearEffects, hyperion::Components componentconst, QString origin, unsigned smoothCfg); -private slots: - void effectFinished(); - private: PyObject * json2python(const QJsonValue & jsonData) const; @@ -88,8 +78,6 @@ private: void addImage(); - PyThreadState * _mainThreadState; - const int _priority; const int _timeout; @@ -104,19 +92,17 @@ private: PyThreadState * _interpreterThreadState; - bool _abortRequested; - /// The processor for translating images to led-values ImageProcessor * _imageProcessor; /// Buffer for colorData QVector _colors; - - + + Logger* _log; + QString _origin; QSize _imageSize; QImage _image; QPainter* _painter; QVector _imageStack; }; - diff --git a/libsrc/effectengine/EffectEngine.cpp b/libsrc/effectengine/EffectEngine.cpp index ae367c9e..d5dac8ed 100644 --- a/libsrc/effectengine/EffectEngine.cpp +++ b/libsrc/effectengine/EffectEngine.cpp @@ -11,7 +11,7 @@ // hyperion util includes #include -#include +#include #include // effect engine includes @@ -74,95 +74,21 @@ const std::list &EffectEngine::getActiveEffects() bool EffectEngine::loadEffectDefinition(const QString &path, const QString &effectConfigFile, EffectDefinition & effectDefinition) { - Logger * log = Logger::getInstance("EFFECTENGINE"); - QString fileName = path + QDir::separator() + effectConfigFile; - QJsonParseError error; - // ---------- Read the effect json config file ---------- - - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) - { - Error( log, "Effect file '%s' could not be loaded", QSTRING_CSTR(fileName)); + // Read and parse the effect json config file + QJsonObject configEffect; + if(!JsonUtils::readFile(fileName, configEffect, _log)) return false; - } - - QByteArray fileContent = file.readAll(); - QJsonDocument configEffect = QJsonDocument::fromJson(fileContent, &error); - - if (error.error != QJsonParseError::NoError) - { - // report to the user the failure and their locations in the document. - int errorLine(0), errorColumn(0); - - for( int i=0, count=qMin( error.offset,fileContent.size()); igetRootPath()); } for(auto efx : disabledEfx) { @@ -309,7 +202,7 @@ void EffectEngine::readEffects() if (loadEffectDefinition(path, filename, def)) { InfoIf(availableEffects.find(def.name) != availableEffects.end(), _log, - "effect overload effect '%s' is now taken from %s'", QSTRING_CSTR(def.name), QSTRING_CSTR(path) ); + "effect overload effect '%s' is now taken from '%s'", QSTRING_CSTR(def.name), QSTRING_CSTR(path) ); if ( disableList.contains(def.name) ) { @@ -326,7 +219,7 @@ void EffectEngine::readEffects() // collect effect schemas efxCount = 0; - directory = path + "schema/"; + directory = path.endsWith("/") ? (path + "schema/") : (path + "/schema/"); QStringList pynames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); for (const QString & pyname : pynames) { @@ -387,9 +280,9 @@ int EffectEngine::runEffectScript(const QString &script, const QString &name, co channelCleared(priority); // create the effect - Effect * effect = new Effect(_mainThreadState, priority, timeout, script, name, args, origin, smoothCfg); + Effect * effect = new Effect(priority, timeout, script, name, args, origin, smoothCfg); connect(effect, SIGNAL(setColors(int,std::vector,int,bool,hyperion::Components,const QString,unsigned)), _hyperion, SLOT(setColors(int,std::vector,int,bool,hyperion::Components,const QString,unsigned)), Qt::QueuedConnection); - connect(effect, SIGNAL(effectFinished(Effect*)), this, SLOT(effectFinished(Effect*))); + connect(effect, &QThread::finished, this, &EffectEngine::effectFinished); _activeEffects.push_back(effect); // start the effect @@ -405,7 +298,7 @@ void EffectEngine::channelCleared(int priority) { if (effect->getPriority() == priority) { - effect->abort(); + effect->requestInterruption(); } } } @@ -414,13 +307,17 @@ void EffectEngine::allChannelsCleared() { for (Effect * effect : _activeEffects) { - effect->abort(); + if (effect->getPriority() != 254) + { + effect->requestInterruption(); + } } } -void EffectEngine::effectFinished(Effect *effect) +void EffectEngine::effectFinished() { - if (!effect->isAbortRequested()) + Effect* effect = qobject_cast(sender()); + if (!effect->isInterruptionRequested()) { // effect stopped by itself. Clear the channel _hyperion->clear(effect->getPriority()); diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index a66b78e8..18804617 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -35,11 +35,11 @@ Hyperion* Hyperion::_hyperion = nullptr; -Hyperion* Hyperion::initInstance(const QJsonObject& qjsonConfig, const QString configFile) // REMOVE jsonConfig variable when the conversion from jsonCPP to QtJSON is finished +Hyperion* Hyperion::initInstance(const QJsonObject& qjsonConfig, const QString configFile, const QString rootPath) { if ( Hyperion::_hyperion != nullptr ) throw std::runtime_error("Hyperion::initInstance can be called only one time"); - Hyperion::_hyperion = new Hyperion(qjsonConfig, configFile); + Hyperion::_hyperion = new Hyperion(qjsonConfig, configFile, rootPath); return Hyperion::_hyperion; } @@ -381,7 +381,7 @@ MessageForwarder * Hyperion::getForwarder() return _messageForwarder; } -Hyperion::Hyperion(const QJsonObject &qjsonConfig, const QString configFile) +Hyperion::Hyperion(const QJsonObject &qjsonConfig, const QString configFile, const QString rootPath) : _ledString(createLedString(qjsonConfig["leds"], createColorOrder(qjsonConfig["device"].toObject()))) , _ledStringClone(createLedStringClone(qjsonConfig["leds"], createColorOrder(qjsonConfig["device"].toObject()))) , _muxer(_ledString.leds().size()) @@ -390,6 +390,7 @@ Hyperion::Hyperion(const QJsonObject &qjsonConfig, const QString configFile) , _messageForwarder(createMessageForwarder(qjsonConfig["forwarder"].toObject())) , _qjsonConfig(qjsonConfig) , _configFile(configFile) + , _rootPath(rootPath) , _timer() , _timerBonjourResolver() , _log(CORE_LOGGER) @@ -439,7 +440,7 @@ Hyperion::Hyperion(const QJsonObject &qjsonConfig, const QString configFile) _timerBonjourResolver.start(); // create the effect engine, must be initialized after smoothing! - _effectEngine = new EffectEngine(this,qjsonConfig["effects"].toObject() ); + _effectEngine = new EffectEngine(this,qjsonConfig["effects"].toObject()); const QJsonObject& device = qjsonConfig["device"].toObject(); unsigned int hwLedCount = device["ledCount"].toInt(getLedCount()); @@ -468,9 +469,6 @@ Hyperion::Hyperion(const QJsonObject &qjsonConfig, const QString configFile) _fsi_timer.setSingleShot(true); _fsi_blockTimer.setSingleShot(true); - const QJsonObject & generalConfig = qjsonConfig["general"].toObject(); - _configVersionId = generalConfig["configVersion"].toInt(-1); - // initialize the leds update(); } diff --git a/libsrc/hyperion/schema/schema-boblightServer.json b/libsrc/hyperion/schema/schema-boblightServer.json index 22758038..04e74161 100644 --- a/libsrc/hyperion/schema/schema-boblightServer.json +++ b/libsrc/hyperion/schema/schema-boblightServer.json @@ -15,7 +15,7 @@ "type" : "integer", "required" : true, "title" : "edt_conf_general_port_title", - "minimum" : 0, + "minimum" : 1024, "maximum" : 65535, "propertyOrder" : 2 }, diff --git a/libsrc/hyperion/schema/schema-effects.json b/libsrc/hyperion/schema/schema-effects.json index f5fa2767..4e018a50 100644 --- a/libsrc/hyperion/schema/schema-effects.json +++ b/libsrc/hyperion/schema/schema-effects.json @@ -7,7 +7,7 @@ { "type" : "array", "title" : "edt_conf_effp_paths_title", - "default" : ["../custom-effects"], + "default" : ["$ROOT/custom-effects"], "items" : { "type": "string", "title" : "edt_conf_effp_paths_itemtitle" diff --git a/libsrc/hyperion/schema/schema-jsonServer.json b/libsrc/hyperion/schema/schema-jsonServer.json index 722f7f4f..798acf7c 100644 --- a/libsrc/hyperion/schema/schema-jsonServer.json +++ b/libsrc/hyperion/schema/schema-jsonServer.json @@ -9,7 +9,7 @@ "type" : "integer", "required" : true, "title" : "edt_conf_general_port_title", - "minimum" : 0, + "minimum" : 1024, "maximum" : 65535, "default" : 19444 } diff --git a/libsrc/hyperion/schema/schema-leds.json b/libsrc/hyperion/schema/schema-leds.json index ed31d7f5..146d77bc 100644 --- a/libsrc/hyperion/schema/schema-leds.json +++ b/libsrc/hyperion/schema/schema-leds.json @@ -1,6 +1,7 @@ { "type":"array", "required":true, + "minItems":1, "items": { "type":"object", diff --git a/libsrc/hyperion/schema/schema-protoServer.json b/libsrc/hyperion/schema/schema-protoServer.json index f4665bad..a344ba06 100644 --- a/libsrc/hyperion/schema/schema-protoServer.json +++ b/libsrc/hyperion/schema/schema-protoServer.json @@ -9,7 +9,7 @@ "type" : "integer", "required" : true, "title" : "edt_conf_general_port_title", - "minimum" : 0, + "minimum" : 1024, "maximum" : 65535, "default" : 19445 } diff --git a/libsrc/hyperion/schema/schema-webConfig.json b/libsrc/hyperion/schema/schema-webConfig.json index b8590a83..6440dc76 100644 --- a/libsrc/hyperion/schema/schema-webConfig.json +++ b/libsrc/hyperion/schema/schema-webConfig.json @@ -22,9 +22,9 @@ { "type" : "integer", "title" : "edt_conf_general_port_title", - "minimum" : 0, + "minimum" : 80, "maximum" : 65535, - "default" : 8099, + "default" : 8090, "access" : "expert", "propertyOrder" : 3 } diff --git a/libsrc/jsonserver/JsonClientConnection.cpp b/libsrc/jsonserver/JsonClientConnection.cpp index 66f6b3ad..7a7ea05f 100644 --- a/libsrc/jsonserver/JsonClientConnection.cpp +++ b/libsrc/jsonserver/JsonClientConnection.cpp @@ -25,7 +25,7 @@ JsonClientConnection::JsonClientConnection(QTcpSocket *socket) connect(_socket, SIGNAL(readyRead()), this, SLOT(readData())); // create a new instance of JsonProcessor - _jsonProcessor = new JsonProcessor(_clientAddress.toString()); + _jsonProcessor = new JsonProcessor(_clientAddress.toString(), _log); // get the callback messages from JsonProcessor and send it to the client connect(_jsonProcessor,SIGNAL(callbackMessage(QJsonObject)),this,SLOT(sendMessage(QJsonObject))); } @@ -65,7 +65,7 @@ void JsonClientConnection::readData() void JsonClientConnection::handleRawJsonData() { - _receiveBuffer += _socket->readAll(); + _receiveBuffer += _socket->readAll(); // raw socket data, handling as usual int bytes = _receiveBuffer.indexOf('\n') + 1; while(bytes > 0) @@ -89,7 +89,7 @@ void JsonClientConnection::getWsFrameHeader(WebSocketHeader* header) char fin_rsv_opcode, mask_length; _socket->getChar(&fin_rsv_opcode); _socket->getChar(&mask_length); - + header->fin = (fin_rsv_opcode & BHB0_FIN) == BHB0_FIN; header->opCode = fin_rsv_opcode & BHB0_OPCODE; header->masked = (mask_length & BHB1_MASK) == BHB1_MASK; @@ -116,7 +116,7 @@ void JsonClientConnection::getWsFrameHeader(WebSocketHeader* header) } break; } - + // if the data is masked we need to get the key for unmasking if (header->masked) { @@ -240,7 +240,7 @@ void JsonClientConnection::sendClose(int status, QString reason) ErrorIf(!reason.isEmpty(), _log, QSTRING_CSTR(reason)); _receiveBuffer.clear(); QByteArray sendBuffer; - + sendBuffer.append(136+(status-1000)); int length = reason.size(); if(length >= 126) @@ -257,7 +257,7 @@ void JsonClientConnection::sendClose(int status, QString reason) { sendBuffer.append(quint8(length)); } - + sendBuffer.append(reason); _socket->write(sendBuffer); @@ -282,7 +282,7 @@ void JsonClientConnection::doWebSocketHandshake() QByteArray hash = QCryptographicHash::hash(value, QCryptographicHash::Sha1).toBase64(); // prepare an answer - QString data + QString data = QString("HTTP/1.1 101 Switching Protocols\r\n") + QString("Upgrade: websocket\r\n") + QString("Connection: Upgrade\r\n") @@ -304,7 +304,7 @@ void JsonClientConnection::socketClosed() QByteArray JsonClientConnection::makeFrameHeader(quint8 opCode, quint64 payloadLength, bool lastFrame) { QByteArray header; - + if (payloadLength <= 0x7FFFFFFFFFFFFFFFULL) { //FIN, RSV1-3, opcode (RSV-1, RSV-2 and RSV-3 are zero) @@ -375,9 +375,10 @@ qint64 JsonClientConnection::sendMessage_Websockets(QByteArray &data) quint64 position = i * FRAME_SIZE_IN_BYTES; quint32 frameSize = (payloadSize-position >= FRAME_SIZE_IN_BYTES) ? FRAME_SIZE_IN_BYTES : (payloadSize-position); - + QByteArray buf = makeFrameHeader(OPCODE::TEXT, frameSize, isLastFrame); sendMessage_Raw(buf); + qint64 written = sendMessage_Raw(payload+position,frameSize); if (written > 0) { @@ -412,11 +413,10 @@ void JsonClientConnection::handleBinaryMessage(QByteArray &data) Error(_log, "data size is not multiple of width"); return; } - + Image image; image.resize(width, height); memcpy(image.memptr(), data.data()+4, imgSize); _hyperion->setImage(priority, image, duration_s*1000); } - diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index 16949c5b..e12a3277 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -8,6 +8,7 @@ #include #include "hyperion/Hyperion.h" +#include LedDeviceRegistry LedDevice::_ledDeviceMap = LedDeviceRegistry(); QString LedDevice::_activeDevice = ""; @@ -91,7 +92,6 @@ QJsonObject LedDevice::getLedDeviceSchemas() { // make sure the resources are loaded (they may be left out after static linking) Q_INIT_RESOURCE(LedDeviceSchemas); - QJsonParseError error; // read the json schema from the resource QDir d(":/leddevices/"); @@ -100,40 +100,22 @@ QJsonObject LedDevice::getLedDeviceSchemas() for(QString &item : l) { - QFile schemaData(QString(":/leddevices/")+item); + QString schemaPath(QString(":/leddevices/")+item); QString devName = item.remove("schema-"); - if (!schemaData.open(QIODevice::ReadOnly)) + QString data; + if(!FileUtils::readFile(schemaPath, data, Logger::getInstance("LedDevice"))) { - Error(Logger::getInstance("LedDevice"), "Schema not found: %s", QSTRING_CSTR(item)); throw std::runtime_error("ERROR: Schema not found: " + item.toStdString()); } - QByteArray schema = schemaData.readAll(); - QJsonDocument doc = QJsonDocument::fromJson(schema, &error); - schemaData.close(); - - if (error.error != QJsonParseError::NoError) + QJsonObject schema; + if(!JsonUtils::parse(schemaPath, data, schema, Logger::getInstance("LedDevice"))) { - // report to the user the failure and their locations in the document. - int errorLine(0), errorColumn(0); - - for( int i=0, count=qMin( error.offset,schema.size()); i +// qt incl #include +#include + +// hyperion include +#include namespace FileUtils { - -QString getBaseName( QString sourceFile) -{ - QFileInfo fi( sourceFile ); - return fi.fileName(); -} - -QString getDirName( QString sourceFile) -{ - QFileInfo fi( sourceFile ); - return fi.path(); -} - -}; \ No newline at end of file + QString getBaseName( QString sourceFile) + { + QFileInfo fi( sourceFile ); + return fi.fileName(); + } + + QString getDirName( QString sourceFile) + { + QFileInfo fi( sourceFile ); + return fi.path(); + } + + bool fileExists(const QString& path, Logger* log, bool ignError) + { + QFile file(path); + if(!file.exists()) + { + ErrorIf((!ignError), log,"File does not exist: %s",QSTRING_CSTR(path)); + return false; + } + return true; + } + + bool readFile(const QString& path, QString& data, Logger* log, bool ignError) + { + QFile file(path); + if(!fileExists(path, log, ignError)) + { + return false; + } + + if(!file.open(QFile::ReadOnly | QFile::Text)) + { + if(!ignError) + resolveFileError(file,log); + return false; + } + data = QString(file.readAll()); + file.close(); + + return true; + } + + bool writeFile(const QString& path, const QByteArray& data, Logger* log) + { + QFile file(path); + if (!file.open(QFile::WriteOnly | QFile::Truncate)) + { + resolveFileError(file,log); + return false; + } + + if(file.write(data) == -1) + { + resolveFileError(file,log); + return false; + } + + file.close(); + return true; + } + + bool removeFile(const QString& path, Logger* log) + { + QFile file(path); + if(!file.remove()) + { + resolveFileError(file,log); + return false; + } + return true; + } + + QString convertPath(const QString path) + { + QString p = path; + return p.replace(QString("$ROOT"), Hyperion::getInstance()->getRootPath()); + } + + void resolveFileError(const QFile& file, Logger* log) + { + QFile::FileError error = file.error(); + const char* fn = QSTRING_CSTR(file.fileName()); + switch(error) + { + case QFileDevice::NoError: + Debug(log,"No error occurred while procesing file: %s",fn); + break; + case QFileDevice::ReadError: + Error(log,"Can't read file: %s",fn); + break; + case QFileDevice::WriteError: + Error(log,"Can't write file: %s",fn); + break; + case QFileDevice::FatalError: + Error(log,"Fatal error while processing file: %s",fn); + break; + case QFileDevice::ResourceError: + Error(log,"Resource Error while processing file: %s",fn); + break; + case QFileDevice::OpenError: + Error(log,"Can't open file: %s",fn); + break; + case QFileDevice::AbortError: + Error(log,"Abort Error while processing file: %s",fn); + break; + case QFileDevice::TimeOutError: + Error(log,"Timeout Error while processing file: %s",fn); + break; + case QFileDevice::UnspecifiedError: + Error(log,"Unspecified Error while processing file: %s",fn); + break; + case QFileDevice::RemoveError: + Error(log,"Failed to remove file: %s",fn); + break; + case QFileDevice::RenameError: + Error(log,"Failed to rename file: %s",fn); + break; + case QFileDevice::PositionError: + Error(log,"Position Error while processing file: %s",fn); + break; + case QFileDevice::ResizeError: + Error(log,"Resize Error while processing file: %s",fn); + break; + case QFileDevice::PermissionsError: + Error(log,"Permission Error at file: %s",fn); + break; + case QFileDevice::CopyError: + Error(log,"Error during file copy of file: %s",fn); + break; + default: + break; + } + } +}; diff --git a/libsrc/utils/JsonProcessor.cpp b/libsrc/utils/JsonProcessor.cpp index f99a727e..e35ff14f 100644 --- a/libsrc/utils/JsonProcessor.cpp +++ b/libsrc/utils/JsonProcessor.cpp @@ -1,4 +1,3 @@ - // project includes #include @@ -12,13 +11,11 @@ #include #include #include -#include -#include -#include -#include #include #include #include +#include +#include #include #include @@ -32,24 +29,25 @@ #include #include #include +#include using namespace hyperion; std::map JsonProcessor::_componentsPrevState; -JsonProcessor::JsonProcessor(QString peerAddress, bool noListener) +JsonProcessor::JsonProcessor(QString peerAddress, Logger* log, bool noListener) : QObject() - , _peerAddress(peerAddress) - , _log(Logger::getInstance("JSONRPCPROCESSOR")) + , _peerAddress(peerAddress) + , _log(log) , _hyperion(Hyperion::getInstance()) , _imageProcessor(ImageProcessorFactory::getInstance().newImageProcessor()) , _streaming_logging_activated(false) , _image_stream_timeout(0) { - // notify hyperion about a jsonMessageForward - connect(this, &JsonProcessor::forwardJsonMessage, _hyperion, &Hyperion::forwardJsonMessage); - // notify hyperion about a push emit - connect(this, &JsonProcessor::pushReq, _hyperion, &Hyperion::hyperionStateChanged); + // notify hyperion about a jsonMessageForward + connect(this, &JsonProcessor::forwardJsonMessage, _hyperion, &Hyperion::forwardJsonMessage); + // notify hyperion about a push emit TODO: Remove! Make sure that the target of the commands trigger this (less error margin) instead this instance + connect(this, &JsonProcessor::pushReq, _hyperion, &Hyperion::hyperionStateChanged); if(!noListener) { @@ -73,77 +71,51 @@ void JsonProcessor::handleMessage(const QString& messageString, const QString pe if(!peerAddress.isNull()) _peerAddress = peerAddress; - QString errors; + const QString ident = "JsonRpc@"+_peerAddress; + Q_INIT_RESOURCE(JSONRPC_schemas); + QJsonObject message; + // parse the message + if(!JsonUtils::parse(ident, messageString, message, _log)) + { + sendErrorReply("Errors during message parsing, please consult the Hyperion Log. Data:"+messageString); + return; + } - try - { - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(messageString.toUtf8(), &error); + // check basic message + if(!JsonUtils::validate(ident, message, ":schema", _log)) + { + sendErrorReply("Errors during message validation, please consult the Hyperion Log."); + return; + } - if (error.error != QJsonParseError::NoError) - { - // report to the user the failure and their locations in the document. - int errorLine(0), errorColumn(0); + // check specific message + const QString command = message["command"].toString(); + if(!JsonUtils::validate(ident, message, QString(":schema-%1").arg(command), _log)) + { + sendErrorReply("Errors during specific message validation, please consult the Hyperion Log"); + return; + } - for( int i=0, count=qMin( error.offset,messageString.size()); i 0) + if (!message["args"].toObject().isEmpty()) { - if (!message["args"].toObject().isEmpty()) + QString scriptName; + (message["script"].toString().mid(0, 1) == ":" ) + ? scriptName = ":/effects//" + message["script"].toString().mid(1) + : scriptName = message["script"].toString(); + + std::list effectsSchemas = _hyperion->getEffectSchemas(); + std::list::iterator it = std::find_if(effectsSchemas.begin(), effectsSchemas.end(), find_schema(scriptName)); + + if (it != effectsSchemas.end()) { - QString scriptName; - (message["script"].toString().mid(0, 1) == ":" ) - ? scriptName = ":/effects//" + message["script"].toString().mid(1) - : scriptName = message["script"].toString(); - - std::list effectsSchemas = _hyperion->getEffectSchemas(); - std::list::iterator it = std::find_if(effectsSchemas.begin(), effectsSchemas.end(), find_schema(scriptName)); - - if (it != effectsSchemas.end()) + if(!JsonUtils::validate("JsonRpc@"+_peerAddress, message["args"].toObject(), it->schemaFile, _log)) { - QString errors; + sendErrorReply("Error during arg validation against schema, please consult the Hyperion Log", command, tan); + return; + } - if (!checkJson(message["args"].toObject(), it->schemaFile, errors)) + QJsonObject effectJson; + QJsonArray effectArray; + effectArray = _hyperion->getQJsonConfig()["effects"].toObject()["paths"].toArray(); + + if (effectArray.size() > 0) + { + if (message["name"].toString().trimmed().isEmpty() || message["name"].toString().trimmed().startsWith(".")) { - sendErrorReply("Error while validating json: " + errors, command, tan); + sendErrorReply("Can't save new effect. Effect name is empty or begins with a dot.", command, tan); return; } - QJsonObject effectJson; - QJsonArray effectArray; - effectArray = _hyperion->getQJsonConfig()["effects"].toObject()["paths"].toArray(); + effectJson["name"] = message["name"].toString(); + effectJson["script"] = message["script"].toString(); + effectJson["args"] = message["args"].toObject(); - if (effectArray.size() > 0) + std::list availableEffects = _hyperion->getEffects(); + std::list::iterator iter = std::find_if(availableEffects.begin(), availableEffects.end(), find_effect(message["name"].toString())); + + QFileInfo newFileName; + if (iter != availableEffects.end()) { - if (message["name"].toString().trimmed().isEmpty() || message["name"].toString().trimmed().startsWith(".")) + newFileName.setFile(iter->file); + if (newFileName.absoluteFilePath().mid(0, 1) == ":") { - sendErrorReply("Can't save new effect. Effect name is empty or begins with a dot.", command, tan); + sendErrorReply("The effect name '" + message["name"].toString() + "' is assigned to an internal effect. Please rename your effekt.", command, tan); return; } - - effectJson["name"] = message["name"].toString(); - effectJson["script"] = message["script"].toString(); - effectJson["args"] = message["args"].toObject(); - - std::list availableEffects = _hyperion->getEffects(); - std::list::iterator iter = std::find_if(availableEffects.begin(), availableEffects.end(), find_effect(message["name"].toString())); - - QFileInfo newFileName; - if (iter != availableEffects.end()) - { - newFileName.setFile(iter->file); - if (newFileName.absoluteFilePath().mid(0, 1) == ":") - { - sendErrorReply("The effect name '" + message["name"].toString() + "' is assigned to an internal effect. Please rename your effekt.", command, tan); - return; - } - } else - { - newFileName.setFile(effectArray[0].toString() + QDir::separator() + message["name"].toString().replace(QString(" "), QString("")) + QString(".json")); - - while(newFileName.exists()) - { - newFileName.setFile(effectArray[0].toString() + QDir::separator() + newFileName.baseName() + QString::number(qrand() % ((10) - 0) + 0) + QString(".json")); - } - } - - QJsonFactory::writeJson(newFileName.absoluteFilePath(), effectJson); - Info(_log, "Reload effect list"); - _hyperion->reloadEffects(); - sendSuccessReply(command, tan); } else { - sendErrorReply("Can't save new effect. Effect path empty", command, tan); + QString f = FileUtils::convertPath(effectArray[0].toString() + "/" + message["name"].toString().replace(QString(" "), QString("")) + QString(".json")); + newFileName.setFile(f); + } + + if(!JsonUtils::write(newFileName.absoluteFilePath(), effectJson, _log)) + { + sendErrorReply("Error while saving effect, please check the Hyperion Log", command, tan); return; } + + Info(_log, "Reload effect list"); + _hyperion->reloadEffects(); + sendSuccessReply(command, tan); } else - sendErrorReply("Missing schema file for Python script " + message["script"].toString(), command, tan); + { + sendErrorReply("Can't save new effect. Effect path empty", command, tan); + return; + } } else - sendErrorReply("Missing or empty Object 'args'", command, tan); + sendErrorReply("Missing schema file for Python script " + message["script"].toString(), command, tan); } else - sendErrorReply("Error while parsing json: Message size " + QString(message.size()), command, tan); + sendErrorReply("Missing or empty Object 'args'", command, tan); } void JsonProcessor::handleDeleteEffectCommand(const QJsonObject& message, const QString& command, const int tan) { - if(message.size() > 0) - { - QString effectName = message["name"].toString(); - std::list effectsDefinition = _hyperion->getEffects(); - std::list::iterator it = std::find_if(effectsDefinition.begin(), effectsDefinition.end(), find_effect(effectName)); + QString effectName = message["name"].toString(); + std::list effectsDefinition = _hyperion->getEffects(); + std::list::iterator it = std::find_if(effectsDefinition.begin(), effectsDefinition.end(), find_effect(effectName)); - if (it != effectsDefinition.end()) + if (it != effectsDefinition.end()) + { + QFileInfo effectConfigurationFile(it->file); + if (effectConfigurationFile.absoluteFilePath().mid(0, 1) != ":" ) { - QFileInfo effectConfigurationFile(it->file); - if (effectConfigurationFile.absoluteFilePath().mid(0, 1) != ":" ) + if (effectConfigurationFile.exists()) { - if (effectConfigurationFile.exists()) + bool result = QFile::remove(effectConfigurationFile.absoluteFilePath()); + if (result) { - bool result = QFile::remove(effectConfigurationFile.absoluteFilePath()); - if (result) - { - Info(_log, "Reload effect list"); - _hyperion->reloadEffects(); - sendSuccessReply(command, tan); - } else - sendErrorReply("Can't delete effect configuration file: " + effectConfigurationFile.absoluteFilePath() + ". Please check permissions", command, tan); + Info(_log, "Reload effect list"); + _hyperion->reloadEffects(); + sendSuccessReply(command, tan); } else - sendErrorReply("Can't find effect configuration file: " + effectConfigurationFile.absoluteFilePath(), command, tan); + sendErrorReply("Can't delete effect configuration file: " + effectConfigurationFile.absoluteFilePath() + ". Please check permissions", command, tan); } else - sendErrorReply("Can't delete internal effect: " + message["name"].toString(), command, tan); + sendErrorReply("Can't find effect configuration file: " + effectConfigurationFile.absoluteFilePath(), command, tan); } else - sendErrorReply("Effect " + message["name"].toString() + " not found", command, tan); + sendErrorReply("Can't delete internal effect: " + message["name"].toString(), command, tan); } else - sendErrorReply("Error while parsing json: Message size " + QString(message.size()), command, tan); + sendErrorReply("Effect " + message["name"].toString() + " not found", command, tan); } void JsonProcessor::handleSysInfoCommand(const QJsonObject&, const QString& command, const int tan) @@ -889,14 +852,7 @@ void JsonProcessor::handleConfigGetCommand(const QJsonObject& message, const QSt result["command"] = command; result["tan"] = tan; - try - { - result["result"] = QJsonFactory::readConfig(_hyperion->getConfigFileName()); - } - catch(...) - { - result["result"] = _hyperion->getQJsonConfig(); - } + result["result"] = _hyperion->getQJsonConfig(); // send the result emit callbackMessage(result); @@ -1146,62 +1102,6 @@ void JsonProcessor::sendErrorReply(const QString &error, const QString &command, emit callbackMessage(reply); } -bool JsonProcessor::checkJson(const QJsonObject& message, const QString& schemaResource, QString& errorMessage, bool ignoreRequired) -{ - // make sure the resources are loaded (they may be left out after static linking) - Q_INIT_RESOURCE(JSONRPC_schemas); - QJsonParseError error; - - // read the json schema from the resource - QFile schemaData(schemaResource); - if (!schemaData.open(QIODevice::ReadOnly)) - { - errorMessage = "Schema error: " + schemaData.errorString(); - return false; - } - - // create schema checker - QByteArray schema = schemaData.readAll(); - QJsonDocument schemaJson = QJsonDocument::fromJson(schema, &error); - schemaData.close(); - - if (error.error != QJsonParseError::NoError) - { - // report to the user the failure and their locations in the document. - int errorLine(0), errorColumn(0); - - for( int i=0, count=qMin( error.offset,schema.size()); i + +// util includes +#include + +//qt includes +#include +#include +#include + +#include + +namespace JsonUtils { + + bool readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError) + { + QString data; + if(!FileUtils::readFile(path, data, log, ignError)) + return false; + + if(!parse(path, data, obj, log)) + return false; + + return true; + } + + bool readSchema(const QString& path, QJsonObject& obj, Logger* log) + { + QJsonObject schema; + if(!readFile(path, schema, log)) + return false; + + if(!resolveRefs(schema, obj, log)) + return false; + + return true; + } + + bool parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log) + { + //remove Comments in data + QString cleanData = data; + cleanData.remove(QRegularExpression("([^:]?\\/\\/.*)")); + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(cleanData.toUtf8(), &error); + + if (error.error != QJsonParseError::NoError) + { + // report to the user the failure and their locations in the document. + int errorLine(0), errorColumn(0); + + for( int i=0, count=qMin( error.offset,cleanData.size()); i; } - + if ( LoggerMap->find(name) == LoggerMap->end() ) { log = new Logger(name,minLevel); @@ -44,7 +44,7 @@ void Logger::deleteInstance(QString name) { if (LoggerMap == nullptr) return; - + if ( name.isEmpty() ) { std::map::iterator it; @@ -99,7 +99,7 @@ Logger::Logger ( QString name, LogLevel minLevel ) const char* _appname_char = getprogname(); #endif _appname = QString(_appname_char).toLower(); - + loggerCount++; @@ -111,7 +111,7 @@ Logger::Logger ( QString name, LogLevel minLevel ) Logger::~Logger() { - Debug(this, "logger '%s' destroyed", QSTRING_CSTR(_name) ); + //Debug(this, "logger '%s' destroyed", QSTRING_CSTR(_name) ); loggerCount--; if ( loggerCount == 0 ) closelog(); diff --git a/libsrc/utils/Stats.cpp b/libsrc/utils/Stats.cpp index 4cb22c53..b78d6fc2 100644 --- a/libsrc/utils/Stats.cpp +++ b/libsrc/utils/Stats.cpp @@ -120,7 +120,7 @@ void Stats::resolveReply(QNetworkReply *reply) bool Stats::trigger(bool set) { - QString path = QDir::homePath()+"/.hyperion/misc/"; + QString path = _hyperion->getRootPath()+"/misc/"; QDir dir; QFile file(path + _hyperion->id); diff --git a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp index 4477b580..60a02e55 100644 --- a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp +++ b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp @@ -197,6 +197,7 @@ void QJsonSchemaChecker::checkType(const QJsonValue & value, const QJsonValue & if (_correct == "modify") QJsonUtils::modify(_autoCorrected, _currentPath, defaultValue); + if (_correct == "") setMessage(type + " expected"); } @@ -222,7 +223,10 @@ void QJsonSchemaChecker::checkProperties(const QJsonObject & value, const QJsonO _error = true; if (_correct == "create") + { QJsonUtils::modify(_autoCorrected, _currentPath, QJsonUtils::create(propertyValue, _ignoreRequired), property); + setMessage("Create property: "+property+" with value: "+propertyValue.toObject().find("default").value().toString()); + } if (_correct == "") setMessage("missing member"); @@ -250,7 +254,10 @@ void QJsonSchemaChecker::checkAdditionalProperties(const QJsonObject & value, co _error = true; if (_correct == "remove") + { QJsonUtils::modify(_autoCorrected, _currentPath); + setMessage("Removed property: "+property); + } if (_correct == "") setMessage("no schema definition"); @@ -280,9 +287,12 @@ void QJsonSchemaChecker::checkMinimum(const QJsonValue & value, const QJsonValue _error = true; if (_correct == "modify") + { (defaultValue != QJsonValue::Null) ? QJsonUtils::modify(_autoCorrected, _currentPath, defaultValue) : QJsonUtils::modify(_autoCorrected, _currentPath, schema); + setMessage("Correct too small value: "+QString::number(value.toDouble())+" to: "+QString::number(defaultValue.toDouble())); + } if (_correct == "") setMessage("value is too small (minimum=" + QString::number(schema.toDouble()) + ")"); @@ -304,9 +314,12 @@ void QJsonSchemaChecker::checkMaximum(const QJsonValue & value, const QJsonValue _error = true; if (_correct == "modify") + { (defaultValue != QJsonValue::Null) ? QJsonUtils::modify(_autoCorrected, _currentPath, defaultValue) : QJsonUtils::modify(_autoCorrected, _currentPath, schema); + setMessage("Correct too large value: "+QString::number(value.toDouble())+" to: "+QString::number(defaultValue.toDouble())); + } if (_correct == "") setMessage("value is too large (maximum=" + QString::number(schema.toDouble()) + ")"); @@ -328,10 +341,12 @@ void QJsonSchemaChecker::checkMinLength(const QJsonValue & value, const QJsonVal _error = true; if (_correct == "modify") + { (defaultValue != QJsonValue::Null) ? QJsonUtils::modify(_autoCorrected, _currentPath, defaultValue) : QJsonUtils::modify(_autoCorrected, _currentPath, schema); - + setMessage("Correct too short value: "+value.toString()+" to: "+defaultValue.toString()); + } if (_correct == "") setMessage("value is too short (minLength=" + QString::number(schema.toInt()) + ")"); } @@ -352,10 +367,12 @@ void QJsonSchemaChecker::checkMaxLength(const QJsonValue & value, const QJsonVal _error = true; if (_correct == "modify") + { (defaultValue != QJsonValue::Null) ? QJsonUtils::modify(_autoCorrected, _currentPath, defaultValue) : QJsonUtils::modify(_autoCorrected, _currentPath, schema); - + setMessage("Correct too long value: "+value.toString()+" to: "+defaultValue.toString()); + } if (_correct == "") setMessage("value is too long (maxLength=" + QString::number(schema.toInt()) + ")"); } @@ -375,7 +392,10 @@ void QJsonSchemaChecker::checkItems(const QJsonValue & value, const QJsonObject if (_correct == "remove") if (jArray.isEmpty()) + { QJsonUtils::modify(_autoCorrected, _currentPath); + setMessage("Remove empty array"); + } for(int i = 0; i < jArray.size(); ++i) { @@ -402,9 +422,12 @@ void QJsonSchemaChecker::checkMinItems(const QJsonValue & value, const QJsonValu _error = true; if (_correct == "modify") + { (defaultValue != QJsonValue::Null) ? QJsonUtils::modify(_autoCorrected, _currentPath, defaultValue) : QJsonUtils::modify(_autoCorrected, _currentPath, schema); + setMessage("Correct minItems: "+QString::number(jArray.size())+" to: "+QString::number(defaultValue.toArray().size())); + } if (_correct == "") setMessage("array is too small (minimum=" + QString::number(schema.toInt()) + ")"); @@ -427,9 +450,12 @@ void QJsonSchemaChecker::checkMaxItems(const QJsonValue & value, const QJsonValu _error = true; if (_correct == "modify") + { (defaultValue != QJsonValue::Null) ? QJsonUtils::modify(_autoCorrected, _currentPath, defaultValue) : QJsonUtils::modify(_autoCorrected, _currentPath, schema); + setMessage("Correct maxItems: "+QString::number(jArray.size())+" to: "+QString::number(defaultValue.toArray().size())); + } if (_correct == "") setMessage("array is too large (maximum=" + QString::number(schema.toInt()) + ")"); @@ -472,11 +498,11 @@ void QJsonSchemaChecker::checkUniqueItems(const QJsonValue & value, const QJsonV if (removeDuplicates && _correct == "modify") { QJsonArray uniqueItemsArray; - + for(int i = 0; i < jArray.size(); ++i) if (!uniqueItemsArray.contains(jArray[i])) uniqueItemsArray.append(jArray[i]); - + QJsonUtils::modify(_autoCorrected, _currentPath, uniqueItemsArray); } } @@ -501,9 +527,12 @@ void QJsonSchemaChecker::checkEnum(const QJsonValue & value, const QJsonValue & _error = true; if (_correct == "modify") + { (defaultValue != QJsonValue::Null) ? QJsonUtils::modify(_autoCorrected, _currentPath, defaultValue) : QJsonUtils::modify(_autoCorrected, _currentPath, schema.toArray().first()); + setMessage("Correct unknown enum value: "+value.toString()+" to: "+defaultValue.toString()); + } if (_correct == "") { diff --git a/libsrc/webconfig/CgiHandler.cpp b/libsrc/webconfig/CgiHandler.cpp index fe13a785..e4e5d404 100644 --- a/libsrc/webconfig/CgiHandler.cpp +++ b/libsrc/webconfig/CgiHandler.cpp @@ -72,7 +72,7 @@ void CgiHandler::cmd_runscript() { QStringList scriptFilePathList(_args); scriptFilePathList.removeAt(0); - + QString scriptFilePath = scriptFilePathList.join('/'); // relative path not allowed if (scriptFilePath.indexOf("..") >=0) diff --git a/libsrc/webconfig/StaticFileServing.cpp b/libsrc/webconfig/StaticFileServing.cpp index 6dc80cae..979e575a 100644 --- a/libsrc/webconfig/StaticFileServing.cpp +++ b/libsrc/webconfig/StaticFileServing.cpp @@ -60,9 +60,9 @@ void StaticFileServing::onServerStarted (quint16 port) txtRecord ); Debug(_log, "Web Config mDNS responder started"); - + // json-rpc for http - _jsonProcessor = new JsonProcessor(QString("HTTP-API"),true); + _jsonProcessor = new JsonProcessor(QString("HTTP-API"), _log, true); } void StaticFileServing::onServerStopped () { @@ -200,4 +200,3 @@ void StaticFileServing::onRequestNeedsReply (QtHttpRequest * request, QtHttpRepl printErrorToReply (reply, QtHttpReply::MethodNotAllowed,"Unhandled HTTP/1.1 method " % command); } } - diff --git a/src/hyperion-remote/CMakeLists.txt b/src/hyperion-remote/CMakeLists.txt index eadf8b2f..ec3a67f9 100644 --- a/src/hyperion-remote/CMakeLists.txt +++ b/src/hyperion-remote/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME} effectengine commandline + hyperion-utils Qt5::Gui Qt5::Core Qt5::Network) diff --git a/src/hyperion-remote/JsonConnection.cpp b/src/hyperion-remote/JsonConnection.cpp index 7ec79336..9bf50daa 100644 --- a/src/hyperion-remote/JsonConnection.cpp +++ b/src/hyperion-remote/JsonConnection.cpp @@ -14,8 +14,12 @@ // hyperion-remote includes #include "JsonConnection.h" +// util includes +#include + JsonConnection::JsonConnection(const QString & address, bool printJson) : _printJson(printJson) + , _log(Logger::getInstance("REMOTE")) , _socket() { QStringList parts = address.split(":"); @@ -62,7 +66,7 @@ void JsonConnection::setColor(std::vector colors, int priority, int dura rgbValue.append(color.blue()); } command["color"] = rgbValue; - + if (duration > 0) { command["duration"] = duration; @@ -125,37 +129,20 @@ void JsonConnection::setEffect(const QString &effectName, const QString & effect command["origin"] = QString("hyperion-remote"); command["priority"] = priority; effect["name"] = effectName; - + if (effectArgs.size() > 0) { - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(effectArgs.toUtf8() ,&error); - - if (error.error != QJsonParseError::NoError) + QJsonObject effObj; + if(!JsonUtils::parse("hyperion-remote-args", effectArgs, effObj, _log)) { - // report to the user the failure and their locations in the document. - int errorLine(0), errorColumn(0); - - for( int i=0, count=qMin( error.offset,effectArgs.size()); i 0) { command["duration"] = duration; @@ -177,33 +164,16 @@ void JsonConnection::createEffect(const QString &effectName, const QString &effe effect["command"] = QString("create-effect"); effect["name"] = effectName; effect["script"] = effectScript; - + if (effectArgs.size() > 0) { - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(effectArgs.toUtf8() ,&error); - - if (error.error != QJsonParseError::NoError) + QJsonObject effObj; + if(!JsonUtils::parse("hyperion-remote-args", effectScript, effObj, _log)) { - // report to the user the failure and their locations in the document. - int errorLine(0), errorColumn(0); - - for( int i=0, count=qMin( error.offset,effectArgs.size()); i 0) { - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8() ,&error); - - if (error.error != QJsonParseError::NoError) + QJsonObject configObj; + if(!JsonUtils::parse("hyperion-remote-args", jsonString, configObj, _log)) { - // report to the user the failure and their locations in the document. - int errorLine(0), errorColumn(0); - - for( int i=0, count=qMin( error.offset,jsonString.size()); i #include +//forward class decl +class Logger; + /// /// Connection class to setup an connection to the hyperion server and execute commands /// @@ -61,7 +64,7 @@ public: /// @param effectArgs The arguments of the effect /// void createEffect(const QString &effectName, const QString &effectScript, const QString & effectArgs); - + /// /// Delete a effect configuration file (.json) /// @@ -116,7 +119,7 @@ public: void setSourceAutoSelect(); /// - /// Print the current loaded Hyperion configuration file + /// Print the current loaded Hyperion configuration file /// QString getConfig(std::string type); @@ -188,10 +191,13 @@ private: /// bool parseReply(const QJsonObject & reply); -private: /// Flag for printing all send and received json-messages to the standard out bool _printJson; + // Logger class + Logger* _log; + /// The TCP-Socket with the connection to the server QTcpSocket _socket; + }; diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index cf999366..60fd1b3e 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -33,7 +34,9 @@ #include "hyperiond.h" -HyperionDaemon::HyperionDaemon(QString configFile, QObject *parent) +#include + +HyperionDaemon::HyperionDaemon(QString configFile, const QString rootPath, QObject *parent) : QObject(parent) , _log(Logger::getInstance("MAIN")) , _kodiVideoChecker(nullptr) @@ -73,7 +76,7 @@ HyperionDaemon::HyperionDaemon(QString configFile, QObject *parent) WarningIf(_qconfig.contains("logger"), Logger::getInstance("LOGGER"), "Logger settings overridden by command line argument"); } - _hyperion = Hyperion::initInstance(_qconfig, configFile); + _hyperion = Hyperion::initInstance(_qconfig, configFile, rootPath); Info(_log, "Hyperion initialized"); } @@ -145,11 +148,12 @@ void HyperionDaemon::loadConfig(const QString & configFile) Q_INIT_RESOURCE(resource); // read the json schema from the resource - QString schemaFile = ":/hyperion-schema"; QJsonObject schemaJson; try { + //QJsonObject obj; + //JsonUtils::readSchema(schemaFile, obj, _log); schemaJson = QJsonFactory::readSchema(schemaFile); } catch(const std::runtime_error& error) @@ -160,25 +164,31 @@ void HyperionDaemon::loadConfig(const QString & configFile) QJsonSchemaChecker schemaChecker; schemaChecker.setSchema(schemaJson); - _qconfig = QJsonFactory::readConfig(configFile); + if(!JsonUtils::readFile(configFile, _qconfig, _log)) + throw std::runtime_error("Failed to load config!"); + + // validate config with schema and correct it if required QPair validate = schemaChecker.validate(_qconfig); - if (!validate.first && validate.second) + // errors in schema syntax, abort + if (!validate.second) { - Warning(_log,"Errors have been found in the configuration file. Automatic correction is applied"); + foreach (auto & schemaError, schemaChecker.getMessages()) + Error(_log, "Schema Syntax Error: %s", QSTRING_CSTR(schemaError)); + throw std::runtime_error("ERROR: Hyperion schema has errors!"); + } + // errors in configuration, correct it! + if (!validate.first) + { + Warning(_log,"Errors have been found in the configuration file. Automatic correction has been applied"); _qconfig = schemaChecker.getAutoCorrectedConfig(_qconfig); - if (!QJsonFactory::writeJson(configFile, _qconfig)) - throw std::runtime_error("ERROR: can not save configuration file, aborting "); - } - else if (validate.first && !validate.second) //Error in Schema - { - QStringList schemaErrors = schemaChecker.getMessages(); - foreach (auto & schemaError, schemaErrors) - std::cout << schemaError.toStdString() << std::endl; + foreach (auto & schemaError, schemaChecker.getMessages()) + Warning(_log, "Config Fix: %s", QSTRING_CSTR(schemaError)); - throw std::runtime_error("ERROR: Json validation failed"); + if (!JsonUtils::write(configFile, _qconfig, _log)) + throw std::runtime_error("ERROR: Can't save configuration file, aborting"); } } @@ -409,7 +419,7 @@ void HyperionDaemon::createSystemFrameGrabber() #else QString type = grabberConfig["type"].toString("auto"); #endif - + // auto eval of type if ( type == "auto" ) { diff --git a/src/hyperiond/hyperiond.h b/src/hyperiond/hyperiond.h index 68b3ca12..67d011ae 100644 --- a/src/hyperiond/hyperiond.h +++ b/src/hyperiond/hyperiond.h @@ -58,7 +58,7 @@ class HyperionDaemon : public QObject friend SysTray; public: - HyperionDaemon(QString configFile, QObject *parent=nullptr); + HyperionDaemon(QString configFile, QString rootPath, QObject *parent=nullptr); ~HyperionDaemon(); void loadConfig(const QString & configFile); @@ -95,11 +95,11 @@ private: X11Wrapper* _x11Grabber; #endif AmlogicWrapper* _amlGrabber; - FramebufferWrapper* _fbGrabber; + FramebufferWrapper* _fbGrabber; OsxWrapper* _osxGrabber; Hyperion* _hyperion; Stats* _stats; - + unsigned _grabber_width; unsigned _grabber_height; unsigned _grabber_frequency; diff --git a/src/hyperiond/main.cpp b/src/hyperiond/main.cpp index 574fc1e2..a1ade791 100644 --- a/src/hyperiond/main.cpp +++ b/src/hyperiond/main.cpp @@ -92,7 +92,7 @@ QCoreApplication* createApplication(int &argc, char *argv[]) // if x11, then test if xserver is available #ifdef ENABLE_X11 Display* dpy = XOpenDisplay(NULL); - if (dpy != NULL) + if (dpy != NULL) { XCloseDisplay(dpy); isGuiApp = true; @@ -140,6 +140,7 @@ int main(int argc, char** argv) parser.addHelpOption(); BooleanOption & versionOption = parser.add(0x0, "version", "Show version information"); + Option & rootPathOption = parser.add